From e911f8dd5b3e9bc2233886f0b85dd038f9aaa8f9 Mon Sep 17 00:00:00 2001 From: Tim Costa Date: Fri, 12 Aug 2022 22:50:32 -0500 Subject: [PATCH] fix: issues with spawned procs, stdin use, env, output --- README.md | 1 - package.json | 7 +- src/extension.ts | 9 +- src/features/Helpers/configuration.ts | 14 +- src/features/formatter/formattingProvider.ts | 100 ----------- src/features/formatter/process.ts | 60 ------- src/features/providers/format.ts | 75 ++++++++ src/features/providers/lint.ts | 139 +++++++++++++++ src/features/sqlFluffLinter.ts | 19 +- src/features/utils/lintingProvider.ts | 177 ------------------- src/features/utils/sqlfluff.ts | 129 ++++++++++++++ 11 files changed, 368 insertions(+), 362 deletions(-) delete mode 100644 src/features/formatter/formattingProvider.ts delete mode 100644 src/features/formatter/process.ts create mode 100644 src/features/providers/format.ts create mode 100644 src/features/providers/lint.ts delete mode 100644 src/features/utils/lintingProvider.ts create mode 100644 src/features/utils/sqlfluff.ts diff --git a/README.md b/README.md index 176035a..2c5e18b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ DBT setup requires these settings to lint and format the document. ```json "sqlfluff.linter.run": "onSave", - "sqlfluff.experimental.format.executeInTerminal": true, ``` ### Format file diff --git a/package.json b/package.json index 9912504..3baf33e 100644 --- a/package.json +++ b/package.json @@ -102,11 +102,6 @@ "default": true, "description": "Determines if the document formatter is enabled." }, - "sqlfluff.experimental.format.executeInTerminal": { - "type": "boolean", - "default": false, - "markdownDescription": "Determines if the `sqlfluff fix` command overwrites the file contents instead of this extension. You should not change the file contents while formatting is occurring if this is enabled. May lead to problems if `editor.formatOnSave = true`. This allows formatting to work when the templater is set to dbt. This can help solve [Mojibake](https://en.wikipedia.org/wiki/Mojibake) issues." - }, "sqlfluff.linter.run": { "type": "string", "enum": [ @@ -117,7 +112,7 @@ "default": "onType", "description": "Determines if the linter runs on save, on type, or disabled.", "markdownEnumDescriptions": [ - "Run the linter when the document is typed in. Does not work when the templater is set to dbt.", + "Run the linter when the document is typed in. Does not work when the templater is set to dbt, does not evaluate .sqlfluff files nested deeper than the root.", "Run the linter when the document is saved.", "Do not run the linter." ] diff --git a/src/extension.ts b/src/extension.ts index fd781a6..824faf3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,11 +3,12 @@ import * as vscode from "vscode"; import { EXCLUDE_RULE, SQLFLuffDocumentFormattingEditProvider, SQLFluffLinterProvider, SQLFluffQuickFix } from "./features/sqlFluffLinter"; export const activate = (context: vscode.ExtensionContext) => { - new SQLFluffLinterProvider().activate(context.subscriptions); + const outputChannel = vscode.window.createOutputChannel("SQLFluff"); + new SQLFluffLinterProvider(outputChannel).activate(context.subscriptions); - vscode.languages.registerDocumentFormattingEditProvider("sql", new SQLFLuffDocumentFormattingEditProvider().activate()); - vscode.languages.registerDocumentFormattingEditProvider("sql-bigquery", new SQLFLuffDocumentFormattingEditProvider().activate()); - vscode.languages.registerDocumentFormattingEditProvider("jinja-sql", new SQLFLuffDocumentFormattingEditProvider().activate()); + vscode.languages.registerDocumentFormattingEditProvider("sql", new SQLFLuffDocumentFormattingEditProvider(outputChannel).activate()); + vscode.languages.registerDocumentFormattingEditProvider("sql-bigquery", new SQLFLuffDocumentFormattingEditProvider(outputChannel).activate()); + vscode.languages.registerDocumentFormattingEditProvider("jinja-sql", new SQLFLuffDocumentFormattingEditProvider(outputChannel).activate()); context.subscriptions.push( vscode.languages.registerCodeActionsProvider("sql", new SQLFluffQuickFix(), { diff --git a/src/features/Helpers/configuration.ts b/src/features/Helpers/configuration.ts index 44c8790..a278331 100644 --- a/src/features/Helpers/configuration.ts +++ b/src/features/Helpers/configuration.ts @@ -69,12 +69,6 @@ export class Configuration { .get("enabled"); } - public static executeInTerminal(): boolean { - return vscode.workspace - .getConfiguration("sqlfluff.experimental.format") - .get("executeInTerminal"); - } - public static runTrigger(): string { return vscode.workspace .getConfiguration("sqlfluff.linter") @@ -82,19 +76,19 @@ export class Configuration { } public static lintBufferArguments(): string[] { - return ["lint", "--format", "json", "-"]; + return ["--format", "json"]; } public static lintFileArguments(): string[] { - return ["lint", "--format", "json"]; + return ["--format", "json"]; } public static formatBufferArguments(): string[] { - return ["fix", "--force", "-"]; + return ["--force"]; } public static formatFileArguments(): string[] { - return ["fix", "--force"]; + return ["--force"]; } public static extraArguments(): string[] { diff --git a/src/features/formatter/formattingProvider.ts b/src/features/formatter/formattingProvider.ts deleted file mode 100644 index 71a36fe..0000000 --- a/src/features/formatter/formattingProvider.ts +++ /dev/null @@ -1,100 +0,0 @@ -"use strict"; - -import * as cp from "child_process"; -import * as fs from "fs"; -import * as vscode from "vscode"; - -import { Configuration } from "../Helpers/configuration"; -import Process from "./process"; - -export class DocumentFormattingEditProvider { - async provideDocumentFormattingEdits( - document: vscode.TextDocument - ): Promise { - const filePath = document.fileName.replace(/\\/g, "/"); - const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath.replace(/\\/g, "/"); - const workingDirectory = Configuration.workingDirectory() ? Configuration.workingDirectory() : rootPath; - const textEdits: vscode.TextEdit[] = []; - - const options = workingDirectory ? - { - cwd: workingDirectory, - env: { - LANG: "en_US.utf-8" - }, - } : undefined; - - if (Configuration.formatEnabled()) { - if (Configuration.executeInTerminal()) { - if (document.isDirty) { - await document.save(); - } - - let args = Configuration.formatFileArguments(); - args = args.concat(Configuration.extraArguments()); - - const command = `${Configuration.executablePath()} ${args.join(" ")} ${filePath}`; - try { - cp.execSync(command, options); - const contents = fs.readFileSync(filePath, "utf-8"); - const lines = contents.split(/\r?\n/); - const lineCount = document.lineCount; - const lastLineRange = document.lineAt(lineCount - 1).range; - const endChar = lastLineRange.end.character; - - if (lines[0].startsWith("NO SAFETY:")) { - lines.shift(); - lines.shift(); - } - - if (lines[0].includes("ENOENT")) { - return []; - } - - if (lines.length > 1 || lines[0] !== "") { - textEdits.push(vscode.TextEdit.replace( - new vscode.Range(0, 0, lineCount, endChar), - lines.join("\n") - )); - } - } catch (error) { - vscode.window.showErrorMessage("SQLFluff Formatting Failed."); - } - } else { - const command = Configuration.executablePath(); - - let args = Configuration.formatBufferArguments(); - args = args.concat(Configuration.extraArguments()); - - const output = await Process.run(command, args, options, document); - const lines = output.split(/\r?\n/); - const lineCount = document.lineCount; - const lastLineRange = document.lineAt(lineCount - 1).range; - const endChar = lastLineRange.end.character; - - if (lines[0].startsWith("NO SAFETY:")) { - lines.shift(); - lines.shift(); - } - - if (lines[0].includes("ENOENT")) { - return []; - } - - if (lines[0].includes("templating/parsing errors found")) { - vscode.window.showErrorMessage("SQLFluff templating/parsing errors found."); - return []; - } - - if (lines.length > 1 || lines[0] !== "") { - textEdits.push(vscode.TextEdit.replace( - new vscode.Range(0, 0, lineCount, endChar), - lines.join("\n") - )); - } - } - } - - return textEdits; - } -} diff --git a/src/features/formatter/process.ts b/src/features/formatter/process.ts deleted file mode 100644 index a1f2721..0000000 --- a/src/features/formatter/process.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ChildProcess, spawn, SpawnOptions } from "child_process"; -import { StringDecoder } from "string_decoder"; -import { TextDocument } from "vscode"; - -export default class Process { - static process: ChildProcess | null = null; - - /** - * Creates a child process to execute a command. - * - * @param command - The command to run. - * @param args - A list of string arguments. - * @param options - The working directory and environment variables. - * @returns The output of the child process. - */ - static run(command: string, args: string[], options: SpawnOptions, document: TextDocument): Promise { - return new Promise((resolve) => { - const buffers: any[] = []; - - const onData = (data: Buffer) => { - buffers.push(data); - }; - - const onClose = () => { - const encoding: BufferEncoding = "utf8"; - const stringDecoder = new StringDecoder(encoding); - - const output = buffers.reduce((response, buffer) => { - response += stringDecoder.write(buffer); - return (response); - }, ""); - - resolve(output); - }; - - if (this.process) { - this.process.kill(); - } - - this.process = spawn( - command, - args, - options - ); - - if (this.process && this.process.pid) { - this.process.stdin.write(document.getText()); - this.process.stdin.end(); - - this.process.stdout.on("data", onData); - - this.process.on("error", (error: any) => { - resolve(error.message); - }); - - this.process.on("close", onClose); - } - }); - } -} diff --git a/src/features/providers/format.ts b/src/features/providers/format.ts new file mode 100644 index 0000000..c0cf86c --- /dev/null +++ b/src/features/providers/format.ts @@ -0,0 +1,75 @@ +"use strict"; + +import * as cp from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { Configuration } from "../helpers/configuration"; +import { SQLFluff, SQLFluffCommand } from "../utils/sqlfluff"; + +export class DocumentFormattingEditProvider { + public outputChannel: vscode.OutputChannel; + public runner: SQLFluff; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + this.runner = new SQLFluff(this.outputChannel); + } + async provideDocumentFormattingEdits( + document: vscode.TextDocument + ): Promise { + const filePath = document.fileName.replace(/\\/g, "/"); + const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath.replace(/\\/g, "/"); + const workingDirectory = Configuration.workingDirectory() ? Configuration.workingDirectory() : rootPath; + const textEdits: vscode.TextEdit[] = []; + this.outputChannel.appendLine(`Format triggered for ${filePath}`); + + if (Configuration.formatEnabled()) { + if (document.isDirty) { + await document.save(); + } + + try { + const result = await this.runner.run( + workingDirectory, + SQLFluffCommand.FIX, + Configuration.formatFileArguments(), + { + targetFileFullPath: filePath, + }, + ); + if (!result.succeeded) { + throw new Error('Command failed to execute, check logs for details'); + } + const contents = fs.readFileSync(filePath, "utf-8"); + const lines = contents.split(/\r?\n/); + const lineCount = document.lineCount; + const lastLineRange = document.lineAt(lineCount - 1).range; + const endChar = lastLineRange.end.character; + + if (lines[0].startsWith("NO SAFETY:")) { + lines.shift(); + lines.shift(); + } + + if (lines[0].includes("ENOENT")) { + return []; + } + + if (lines.length > 1 || lines[0] !== "") { + textEdits.push(vscode.TextEdit.replace( + new vscode.Range(0, 0, lineCount, endChar), + lines.join("\n") + )); + } + } catch (error) { + vscode.window.showErrorMessage(`SQLFluff Formatting Failed: ${error.message}`); + } + } else { + this.outputChannel.appendLine("Skipping format, not enabled in settings"); + } + + return textEdits; + } +} diff --git a/src/features/providers/lint.ts b/src/features/providers/lint.ts new file mode 100644 index 0000000..aab1599 --- /dev/null +++ b/src/features/providers/lint.ts @@ -0,0 +1,139 @@ +"use strict"; +import * as cp from "child_process"; +import * as vscode from "vscode"; +import * as path from "path"; + +import { Configuration } from "../helpers/configuration"; +import { ThrottledDelayer } from "../utils/async"; +import { LineDecoder } from "../utils/lineDecoder"; +import { SQLFluff, SQLFluffCommand, SQLFluffCommandOptions } from "../utils/sqlfluff"; + +enum RunTrigger { + onSave = "onSave", + onType = "onType", + off = "off" +} + +export interface Linter { + languageId: Array, + process: (output: string[]) => vscode.Diagnostic[]; +} + +export class LintingProvider { + public outputChannel: vscode.OutputChannel; + public oldExecutablePath: string; + public runner: SQLFluff; + + private executableNotFound: boolean; + private documentListener!: vscode.Disposable; + private diagnosticCollection!: vscode.DiagnosticCollection; + private delayers!: { [key: string]: ThrottledDelayer; }; + private linter: Linter; + private childProcess: cp.ChildProcess; + + constructor(outputChannel: vscode.OutputChannel, linter: Linter) { + this.linter = linter; + this.executableNotFound = false; + this.outputChannel = outputChannel; + this.runner = new SQLFluff(this.outputChannel); + } + + public activate(subscriptions: vscode.Disposable[]) { + this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); + subscriptions.push(this); + vscode.workspace.onDidChangeConfiguration(this.loadConfiguration, this, subscriptions); + this.loadConfiguration(); + + vscode.workspace.onDidOpenTextDocument(this.triggerLint, this, subscriptions); + vscode.workspace.onDidCloseTextDocument((textDocument) => { + this.diagnosticCollection.delete(textDocument.uri); + delete this.delayers[textDocument.uri.toString()]; + }, null, subscriptions); + + // Lint all open documents documents + vscode.workspace.textDocuments.forEach(this.triggerLint, this); + } + + public dispose(): void { + this.diagnosticCollection.clear(); + this.diagnosticCollection.dispose(); + } + + private loadConfiguration(): void { + this.delayers = Object.create(null); + + if (this.executableNotFound) { + this.executableNotFound = this.oldExecutablePath === Configuration.executablePath(); + } + + if (this.documentListener) { + this.documentListener.dispose(); + } + + if (Configuration.runTrigger() === RunTrigger.onType) { + this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => { + this.triggerLint(e.document); + }); + } + this.documentListener = vscode.workspace.onDidSaveTextDocument(this.triggerLint, this); + + // Configuration has changed. Reevaluate all documents. + vscode.workspace.textDocuments.forEach(this.triggerLint, this); + } + + private triggerLint(textDocument: vscode.TextDocument): void { + if ( + !this.linter.languageId.includes(textDocument.languageId) + || this.executableNotFound + || Configuration.runTrigger() === RunTrigger.off + ) { + return; + } + + const key = textDocument.uri.toString(); + this.outputChannel.appendLine(`Lint triggered for ${key}`); + let delayer = this.delayers[key]; + + if (!delayer) { + delayer = new ThrottledDelayer(500); + this.delayers[key] = delayer; + } + + delayer.trigger(() => { return this.doLint(textDocument); }); + } + + private async doLint(document: vscode.TextDocument): Promise { + const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath.replace(/\\/g, "/"); + const workingDirectory = Configuration.workingDirectory() || rootPath; + + let args: string[] = []; + let options: SQLFluffCommandOptions = {}; + + if (document.isDirty && Configuration.runTrigger() === RunTrigger.onType) { + // doc is dirty and we run on type, so pass it to buffer input linting + args = [...Configuration.lintBufferArguments()] + options.fileContents = document.getText(); + } else { + // doc is clean (saved) or we are linting onSave, so we should lint from file + args = [...Configuration.lintFileArguments()] + options.targetFileFullPath = document.fileName; + } + + const result = await this.runner.run( + workingDirectory, + SQLFluffCommand.LINT, + args, + options, + ); + + if (!result.succeeded) { + throw new Error('Command failed to execute, check logs for details'); + } + + let diagnostics: vscode.Diagnostic[] = []; + if (result.lines?.length > 0) { + diagnostics = this.linter.process(result.lines); + this.diagnosticCollection.set(document.uri, diagnostics); + } + } +} diff --git a/src/features/sqlFluffLinter.ts b/src/features/sqlFluffLinter.ts index dc43ead..a43967b 100644 --- a/src/features/sqlFluffLinter.ts +++ b/src/features/sqlFluffLinter.ts @@ -2,14 +2,19 @@ import * as vscode from "vscode"; import { Diagnostic, DiagnosticSeverity, Disposable, Range } from "vscode"; -import { DocumentFormattingEditProvider } from "./formatter/formattingProvider"; -import { Linter, LintingProvider } from "./utils/lintingProvider"; +import { DocumentFormattingEditProvider } from "./providers/format"; +import { Linter, LintingProvider } from "./providers/lint"; export class SQLFluffLinterProvider implements Linter { public languageId = ["sql", "jinja-sql", "sql-bigquery"]; + public outputChannel: vscode.OutputChannel; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } public activate(subscriptions: Disposable[]) { - const provider = new LintingProvider(this); + const provider = new LintingProvider(this.outputChannel, this); provider.activate(subscriptions); } @@ -21,6 +26,7 @@ export class SQLFluffLinterProvider implements Linter { filePaths = JSON.parse(line); } catch (e) { // JSON.parse may fail if sqlfluff compilation prints non-JSON formatted messages + this.outputChannel.append(e.toString()); console.warn(e); } @@ -54,8 +60,13 @@ interface FilePath { } export class SQLFLuffDocumentFormattingEditProvider { + public outputChannel: vscode.OutputChannel; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } activate(): vscode.DocumentFormattingEditProvider { - return new DocumentFormattingEditProvider(); + return new DocumentFormattingEditProvider(this.outputChannel); } } diff --git a/src/features/utils/lintingProvider.ts b/src/features/utils/lintingProvider.ts deleted file mode 100644 index ec56bcb..0000000 --- a/src/features/utils/lintingProvider.ts +++ /dev/null @@ -1,177 +0,0 @@ -"use strict"; -import * as cp from "child_process"; -import * as vscode from "vscode"; - -import { Configuration } from "../Helpers/configuration"; -import { ThrottledDelayer } from "./async"; -import { LineDecoder } from "./lineDecoder"; - -enum RunTrigger { - onSave = "onSave", - onType = "onType", - off = "off" -} - -export interface Linter { - languageId: Array, - process: (output: string[]) => vscode.Diagnostic[]; -} - -export class LintingProvider { - public oldExecutablePath: string; - - private executableNotFound: boolean; - private documentListener!: vscode.Disposable; - private diagnosticCollection!: vscode.DiagnosticCollection; - private delayers!: { [key: string]: ThrottledDelayer; }; - private linter: Linter; - private childProcess: cp.ChildProcess; - - constructor(linter: Linter) { - this.linter = linter; - this.executableNotFound = false; - } - - public activate(subscriptions: vscode.Disposable[]) { - this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); - subscriptions.push(this); - vscode.workspace.onDidChangeConfiguration(this.loadConfiguration, this, subscriptions); - this.loadConfiguration(); - - vscode.workspace.onDidOpenTextDocument(this.triggerLint, this, subscriptions); - vscode.workspace.onDidCloseTextDocument((textDocument) => { - this.diagnosticCollection.delete(textDocument.uri); - delete this.delayers[textDocument.uri.toString()]; - }, null, subscriptions); - - // Lint all open documents documents - vscode.workspace.textDocuments.forEach(this.triggerLint, this); - } - - public dispose(): void { - this.diagnosticCollection.clear(); - this.diagnosticCollection.dispose(); - } - - private loadConfiguration(): void { - this.delayers = Object.create(null); - - if (this.executableNotFound) { - this.executableNotFound = this.oldExecutablePath === Configuration.executablePath(); - } - - if (this.documentListener) { - this.documentListener.dispose(); - } - - if (Configuration.runTrigger() === RunTrigger.onType) { - this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => { - this.triggerLint(e.document); - }); - } - this.documentListener = vscode.workspace.onDidSaveTextDocument(this.triggerLint, this); - - // Configuration has changed. Reevaluate all documents. - vscode.workspace.textDocuments.forEach(this.triggerLint, this); - } - - private triggerLint(textDocument: vscode.TextDocument): void { - if ( - !this.linter.languageId.includes(textDocument.languageId) - || this.executableNotFound - || Configuration.runTrigger() === RunTrigger.off - ) { - return; - } - - const key = textDocument.uri.toString(); - let delayer = this.delayers[key]; - - if (!delayer) { - delayer = new ThrottledDelayer(500); - this.delayers[key] = delayer; - } - - delayer.trigger(() => { return this.doLint(textDocument); }); - } - - private doLint(document: vscode.TextDocument): Promise { - return new Promise((resolve) => { - const decoder = new LineDecoder(); - const filePath = document.fileName.replace(/\\/g, "/"); - const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath.replace(/\\/g, "/"); - const workingDirectory = Configuration.workingDirectory() ? Configuration.workingDirectory() : rootPath; - - let args: string[]; - let diagnostics: vscode.Diagnostic[] = []; - - const onDataEvent = (data: Buffer) => { - decoder.write(data); - }; - - const onEndEvent = () => { - decoder.end(); - - const lines = decoder.getLines(); - if (lines && lines.length > 0) { - diagnostics = this.linter.process(lines); - } - - this.diagnosticCollection.set(document.uri, diagnostics); - resolve(); - }; - - const options = workingDirectory ? - { - cwd: workingDirectory, - env: { - LANG: "en_US.utf-8" - } - } : undefined; - - if (Configuration.runTrigger() === RunTrigger.onSave) { - args = [...Configuration.lintFileArguments(), filePath]; - } else { - args = Configuration.lintBufferArguments(); - } - args = args.concat(Configuration.extraArguments()); - - if (this.childProcess) { - this.childProcess.kill(); - } - this.childProcess = cp.spawn(Configuration.executablePath(), args, options); - - this.childProcess.on("error", (error: Error) => { - let message = ""; - if (this.executableNotFound) { - resolve(); - return; - } - - if ((error).code === "ENOENT") { - message = `Cannot lint ${document.fileName}. The executable was not found. Use the 'Executable Path' setting to configure the location of the executable`; - } else { - message = error.message ? error.message : `Failed to run executable using path: ${Configuration.executablePath()}. Reason is unknown.`; - } - - this.oldExecutablePath = Configuration.executablePath(); - this.executableNotFound = true; - vscode.window.showInformationMessage(message); - resolve(); - }); - - if (this.childProcess.pid) { - if (Configuration.runTrigger() === RunTrigger.onType) { - this.childProcess.stdin.write(document.getText()); - this.childProcess.stdin.end(); - } - - this.childProcess.stdout.on("data", onDataEvent); - this.childProcess.stdout.on("end", onEndEvent); - resolve(); - } else { - resolve(); - } - }); - } -} diff --git a/src/features/utils/sqlfluff.ts b/src/features/utils/sqlfluff.ts new file mode 100644 index 0000000..7382e57 --- /dev/null +++ b/src/features/utils/sqlfluff.ts @@ -0,0 +1,129 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import * as childProcess from "child_process"; + +import { Configuration } from "../helpers/configuration" +import { LineDecoder } from "./lineDecoder"; + +export enum SQLFluffCommand { + LINT = 'lint', + FIX = 'fix', +} + +export interface SQLFluffCommandOutput { + succeeded: boolean; + lines: string[]; +} + +export interface SQLFluffCommandOptions { + targetFileFullPath?: string; + fileContents?: string; +} + +export class SQLFluff { + public executableNotFound: boolean; + public outputChannel: vscode.OutputChannel; + private process: childProcess.ChildProcess; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } + + run(cwd: string, command: SQLFluffCommand, args: string[], options: SQLFluffCommandOptions): Promise { + if (!options.fileContents && !options.targetFileFullPath) { + throw new Error('You must supply either a target file path or the file contents to scan'); + } + if (this.process) { + this.process.kill('SIGKILL'); + this.process = undefined; + } + const workspaceFolderRootPath = path.normalize(vscode.workspace.workspaceFolders[0].uri.fsPath); + const workingDirectory = Configuration.workingDirectory() ? Configuration.workingDirectory() : workspaceFolderRootPath; + const normalizedCwd = path.normalize(cwd); + + return new Promise((resolve) => { + const stdoutDecoder = new LineDecoder(); + let stderrLines = []; + + const onStdoutDataEvent = (data: Buffer) => { + stdoutDecoder.write(data); + }; + + const onStderrDataEvent = (data: Buffer) => { + stderrLines.push(data.toString('utf8')); + }; + + const onCloseEvent = (code, signal) => { + this.outputChannel.appendLine(`Received close event, code ${code} signal ${signal}`); + stdoutDecoder.end(); + + const stdoutLines = stdoutDecoder.getLines(); + this.outputChannel.appendLine(`Raw stdout output:`); + this.outputChannel.appendLine(stdoutLines.join('\n')); + this.outputChannel.appendLine(`Raw stderr output:`); + this.outputChannel.appendLine(stderrLines.join('\n')); + + if (stderrLines?.length > 0) { + vscode.window.showErrorMessage(stderrLines.join('\n')); + } + + return resolve({ + succeeded: code === 0 || code === 65, // 0 = all good, 65 = lint passed, but found errors + lines: stdoutLines, + }); + }; + + const shouldUseStdin = !!options.fileContents?.length; + + const finalArgs = [ + command, + ...args, + ...Configuration.extraArguments(), + ]; + + if (shouldUseStdin) { + this.outputChannel.appendLine('Reading from stdin, not file, input may be dirty/partial'); + finalArgs.push('-'); + } else { + this.outputChannel.appendLine('Reading from file, not stdin'); + // we want to use relative path to the file so intermediate sqlfluff config + // files can be found + const normalizedTargetFileFullPath = path.normalize(options.targetFileFullPath); + const targetFileRelativePath = path.relative(normalizedCwd, normalizedTargetFileFullPath); + finalArgs.push(targetFileRelativePath); + } + this.process = childProcess.spawn(Configuration.executablePath(), finalArgs, { + cwd: workingDirectory, + }); + + if (this.process.pid) { + this.process.stdout.on("data", onStdoutDataEvent); + this.process.stderr.on("data", onStderrDataEvent); + this.process.on("close", onCloseEvent); + if (shouldUseStdin) { + this.process.stdin.write(options.fileContents); + this.process.stdin.end(); + } + } + + this.process.on('message', (message) => { + this.outputChannel.appendLine('Received message from child process'); + this.outputChannel.appendLine(message.toString()); + }) + + this.process.on("error", (error: Error) => { + this.outputChannel.appendLine('Child process threw error'); + this.outputChannel.appendLine(error.toString()); + let { message } = error; + + if ((error as any).code === "ENOENT") { + message = "The sqlfluff executable was not found. Use the 'Executable Path' setting to configure the location of the executable, or add it to your PATH."; + } + + this.executableNotFound = true; + vscode.window.showErrorMessage(message); + resolve({ succeeded: false, lines: [] }); + }); + }); + } +}