diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..075cc45 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index 3c84d65..7fe900c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ package-lock.json *.vsix .DS_Store /vscode.d.ts -/vscode.proposed.d.ts \ No newline at end of file +/vscode.proposed.d.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3ac9aeb..c0a2258 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,5 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "dbaeumer.vscode-eslint" - ] + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..ffeaf91 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,11 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2568d6c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement via email to +`admins`@`sqlfluff.com`. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b2da736 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# vscode-sqlfluff - Contributing + +:star2: **First** - thanks for being interested in improving vscode-sqlfluff! :smiley: + +:star2: **Second** - please read and familiarise yourself with both the content of this guide and also our [code of conduct](CODE_OF_CONDUCT.md). + +:star2: **Third** - the best way to get started contributing, is to use the tool in anger and then to submit bugs and features through github. + +:star2: **Fourth** - making sure that our documentation is up to date and useful for new users is really important. If you're a new user, you're in precisely the best position to do this. Pull requests are always welcome with documentationimprovements. + +:star2: **Fifth** - if you're so inclined - pull requests on the codebase are always welcome. Just fork the repo and create your branch from `master`. Please document your contributions in the `CHANGELOG.md`. \ No newline at end of file diff --git a/README.md b/README.md index 998d054..542e6e7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # vscode-sqlfluff -

- VS Marketplace Version - VS Marketplace Installs - VS Marketplace Rating -

+![.github/workflows/ci.yml](https://github.com/dorzey/vscode-sqlfluff/workflows/.github/workflows/ci.yml/badge.svg) A linter and auto-formatter for [SQLFluff](https://github.com/alanmcruickshank/sqlfluff), a popular linting tool for SQL and dbt. diff --git a/src/extension.ts b/src/extension.ts index 657870e..fd781a6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,36 +3,36 @@ 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); + new SQLFluffLinterProvider().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().activate()); + vscode.languages.registerDocumentFormattingEditProvider("sql-bigquery", new SQLFLuffDocumentFormattingEditProvider().activate()); + vscode.languages.registerDocumentFormattingEditProvider("jinja-sql", new SQLFLuffDocumentFormattingEditProvider().activate()); - context.subscriptions.push( - vscode.languages.registerCodeActionsProvider("sql", new SQLFluffQuickFix(), { - providedCodeActionKinds: SQLFluffQuickFix.providedCodeActionKind - }) - ); + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider("sql", new SQLFluffQuickFix(), { + providedCodeActionKinds: SQLFluffQuickFix.providedCodeActionKind + }) + ); - context.subscriptions.push(vscode.commands.registerCommand(EXCLUDE_RULE, toggleRule)); + context.subscriptions.push(vscode.commands.registerCommand(EXCLUDE_RULE, toggleRule)); }; // eslint-disable-next-line @typescript-eslint/no-empty-function export const deactivate: any = () => { }; function toggleRule(rule: string) { - const configuration = vscode.workspace.getConfiguration("sqlfluff"); - const excludeRulesArray: string[] = configuration.get("excludeRules"); + const configuration = vscode.workspace.getConfiguration("sqlfluff"); + const excludeRulesArray: string[] = configuration.get("excludeRules"); - if (!excludeRulesArray.includes(rule)) { - excludeRulesArray.push(rule); - } + if (!excludeRulesArray.includes(rule)) { + excludeRulesArray.push(rule); + } - excludeRulesArray.sort((x, y) => { - return parseInt(x.substring(1)) - parseInt(y.substring(1)); - }); + excludeRulesArray.sort((x, y) => { + return parseInt(x.substring(1)) - parseInt(y.substring(1)); + }); - return configuration.update("excludeRules", excludeRulesArray, vscode.ConfigurationTarget.Global); + return configuration.update("excludeRules", excludeRulesArray, vscode.ConfigurationTarget.Global); } diff --git a/src/features/formatter/process.ts b/src/features/formatter/process.ts index ed71206..bf54207 100644 --- a/src/features/formatter/process.ts +++ b/src/features/formatter/process.ts @@ -7,7 +7,7 @@ export default class Process { /** * 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. diff --git a/src/features/sqlFluffLinter.ts b/src/features/sqlFluffLinter.ts index 2ee6047..dc43ead 100644 --- a/src/features/sqlFluffLinter.ts +++ b/src/features/sqlFluffLinter.ts @@ -6,102 +6,102 @@ import { DocumentFormattingEditProvider } from "./formatter/formattingProvider"; import { Linter, LintingProvider } from "./utils/lintingProvider"; export class SQLFluffLinterProvider implements Linter { - public languageId = ["sql", "jinja-sql", "sql-bigquery"]; + public languageId = ["sql", "jinja-sql", "sql-bigquery"]; - public activate(subscriptions: Disposable[]) { - const provider = new LintingProvider(this); - provider.activate(subscriptions); - } + public activate(subscriptions: Disposable[]) { + const provider = new LintingProvider(this); + provider.activate(subscriptions); + } - public process(lines: string[]): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - lines.forEach((line) => { - let filePaths: Array; - try { - filePaths = JSON.parse(line); - } catch (e) { - // JSON.parse may fail if sqlfluff compilation prints non-JSON formatted messages - console.warn(e); - } + public process(lines: string[]): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + lines.forEach((line) => { + let filePaths: Array; + try { + filePaths = JSON.parse(line); + } catch (e) { + // JSON.parse may fail if sqlfluff compilation prints non-JSON formatted messages + console.warn(e); + } - if (filePaths) { - filePaths.forEach((filePath: FilePath) => { - filePath.violations.forEach((violation: Violation) => { - const diagnostic = new Diagnostic( - new Range( - violation.line_no - 1, - violation.line_pos, - violation.line_no - 1, - violation.line_pos - ), - violation.description, - DiagnosticSeverity.Error, - ); - diagnostic.code = violation.code; - diagnostic.source = "sqlfluff"; - diagnostics.push(diagnostic); - }); - }); - } - }); + if (filePaths) { + filePaths.forEach((filePath: FilePath) => { + filePath.violations.forEach((violation: Violation) => { + const diagnostic = new Diagnostic( + new Range( + violation.line_no - 1, + violation.line_pos, + violation.line_no - 1, + violation.line_pos + ), + violation.description, + DiagnosticSeverity.Error, + ); + diagnostic.code = violation.code; + diagnostic.source = "sqlfluff"; + diagnostics.push(diagnostic); + }); + }); + } + }); - return diagnostics; - } + return diagnostics; + } } interface FilePath { - violations: Array; + violations: Array; } export class SQLFLuffDocumentFormattingEditProvider { - activate(): vscode.DocumentFormattingEditProvider { - return new DocumentFormattingEditProvider(); - } + activate(): vscode.DocumentFormattingEditProvider { + return new DocumentFormattingEditProvider(); + } } interface Violation { - line_no: number, - line_pos: number, - description: string, - code: string, + line_no: number, + line_pos: number, + description: string, + code: string, } export class SQLFluffQuickFix implements vscode.CodeActionProvider { - public static readonly providedCodeActionKind = [ - vscode.CodeActionKind.QuickFix, - ]; + public static readonly providedCodeActionKind = [ + vscode.CodeActionKind.QuickFix, + ]; - provideCodeActions( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - token: vscode.CancellationToken - ): vscode.CodeAction[] { - // for each diagnostic entry that has the matching `code`, create a code action command - return context.diagnostics.map((diagnostic) => - this.createCodeAction(diagnostic) - ); - } + provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + token: vscode.CancellationToken + ): vscode.CodeAction[] { + // for each diagnostic entry that has the matching `code`, create a code action command + return context.diagnostics.map((diagnostic) => + this.createCodeAction(diagnostic) + ); + } - private createCodeAction( - diagnostic: vscode.Diagnostic - ): vscode.CodeAction { - const action = new vscode.CodeAction( - `Exclude Rule ${diagnostic.code}`, - vscode.CodeActionKind.QuickFix - ); - action.command = { - command: EXCLUDE_RULE, - title: `Exclude Rule ${diagnostic.code}`, - tooltip: `This will exclude the rule ${diagnostic.code} in settings.json`, - arguments: [diagnostic.code] - }; - action.diagnostics = [diagnostic]; - action.isPreferred = true; + private createCodeAction( + diagnostic: vscode.Diagnostic + ): vscode.CodeAction { + const action = new vscode.CodeAction( + `Exclude Rule ${diagnostic.code}`, + vscode.CodeActionKind.QuickFix + ); + action.command = { + command: EXCLUDE_RULE, + title: `Exclude Rule ${diagnostic.code}`, + tooltip: `This will exclude the rule ${diagnostic.code} in settings.json`, + arguments: [diagnostic.code] + }; + action.diagnostics = [diagnostic]; + action.isPreferred = true; - return action; - } + return action; + } } export const EXCLUDE_RULE = "sqlfluff.quickfix.command"; diff --git a/src/features/utils/async.ts b/src/features/utils/async.ts index 28d8c57..71cdedd 100644 --- a/src/features/utils/async.ts +++ b/src/features/utils/async.ts @@ -1,125 +1,125 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ export interface ITask { - (): T; + (): T; } /** * A helper to prevent accumulation of sequential async tasks. */ export class Throttler { - private activePromise!: Promise; - private queuedPromise!: Promise; - private queuedPromiseFactory!: ITask>; - - public queue(promiseFactory: ITask>): Promise { - if (this.activePromise) { - this.queuedPromiseFactory = promiseFactory; - - if (!this.queuedPromise) { - const onComplete = () => { - // @ts-ignore - this.queuedPromise = null; - - const result = this.queue(this.queuedPromiseFactory); - // @ts-ignore - this.queuedPromiseFactory = null; - - return result; - }; - - this.queuedPromise = new Promise((resolve, reject) => { - this.activePromise.then(onComplete, onComplete).then(resolve); - }); - } - - return new Promise((resolve, reject) => { - this.queuedPromise.then(resolve, reject); - }); - } - - this.activePromise = promiseFactory(); - - return new Promise((resolve, reject) => { - this.activePromise.then((result: T) => { - // @ts-ignore - this.activePromise = null; - resolve(result); - }, (err: any) => { - // @ts-ignore - this.activePromise = null; - reject(err); - }); - }); - } + private activePromise!: Promise; + private queuedPromise!: Promise; + private queuedPromiseFactory!: ITask>; + + public queue(promiseFactory: ITask>): Promise { + if (this.activePromise) { + this.queuedPromiseFactory = promiseFactory; + + if (!this.queuedPromise) { + const onComplete = () => { + // @ts-ignore + this.queuedPromise = null; + + const result = this.queue(this.queuedPromiseFactory); + // @ts-ignore + this.queuedPromiseFactory = null; + + return result; + }; + + this.queuedPromise = new Promise((resolve, reject) => { + this.activePromise.then(onComplete, onComplete).then(resolve); + }); + } + + return new Promise((resolve, reject) => { + this.queuedPromise.then(resolve, reject); + }); + } + + this.activePromise = promiseFactory(); + + return new Promise((resolve, reject) => { + this.activePromise.then((result: T) => { + // @ts-ignore + this.activePromise = null; + resolve(result); + }, (err: any) => { + // @ts-ignore + this.activePromise = null; + reject(err); + }); + }); + } } /** * A helper to delay execution of a task that is being requested often. */ export class Delayer { - public defaultDelay: number; - private timeout!: number; - private completionPromise!: Promise; - private onResolve!: (value: T | Thenable) => void; - private task!: ITask; - - constructor(defaultDelay: number) { - this.defaultDelay = defaultDelay; - } - - public trigger(task: ITask, delay: number = this.defaultDelay): Promise { - this.task = task; - this.cancelTimeout(); - - if (!this.completionPromise) { - this.completionPromise = new Promise((resolve, reject) => { - this.onResolve = resolve; - }).then(() => { - // @ts-ignore - this.completionPromise = null; - // @ts-ignore - this.onResolve = null; - - const result = this.task(); - // @ts-ignore - this.task = null; - - return result; - }); - } - - // @ts-ignore - this.timeout = setTimeout(() => { - // @ts-ignore - this.timeout = null; - // @ts-ignore - this.onResolve(null); - }, delay); - - return this.completionPromise; - } - - public isTriggered(): boolean { - return this.timeout !== null; - } - - public cancel(): void { - this.cancelTimeout(); - - if (this.completionPromise) { - // @ts-ignore - this.completionPromise = null; - } - } - - private cancelTimeout(): void { - if (this.timeout !== null) { - // @ts-ignore - clearTimeout(this.timeout); - // @ts-ignore - this.timeout = null; - } - } + public defaultDelay: number; + private timeout!: number; + private completionPromise!: Promise; + private onResolve!: (value: T | Thenable) => void; + private task!: ITask; + + constructor(defaultDelay: number) { + this.defaultDelay = defaultDelay; + } + + public trigger(task: ITask, delay: number = this.defaultDelay): Promise { + this.task = task; + this.cancelTimeout(); + + if (!this.completionPromise) { + this.completionPromise = new Promise((resolve, reject) => { + this.onResolve = resolve; + }).then(() => { + // @ts-ignore + this.completionPromise = null; + // @ts-ignore + this.onResolve = null; + + const result = this.task(); + // @ts-ignore + this.task = null; + + return result; + }); + } + + // @ts-ignore + this.timeout = setTimeout(() => { + // @ts-ignore + this.timeout = null; + // @ts-ignore + this.onResolve(null); + }, delay); + + return this.completionPromise; + } + + public isTriggered(): boolean { + return this.timeout !== null; + } + + public cancel(): void { + this.cancelTimeout(); + + if (this.completionPromise) { + // @ts-ignore + this.completionPromise = null; + } + } + + private cancelTimeout(): void { + if (this.timeout !== null) { + // @ts-ignore + clearTimeout(this.timeout); + // @ts-ignore + this.timeout = null; + } + } } /** @@ -127,16 +127,16 @@ export class Delayer { * preventing accumulation of consecutive executions, while the task runs. */ export class ThrottledDelayer extends Delayer> { - private throttler: Throttler; + private throttler: Throttler; - constructor(defaultDelay: number) { - super(defaultDelay); + constructor(defaultDelay: number) { + super(defaultDelay); - this.throttler = new Throttler(); - } + this.throttler = new Throttler(); + } - public trigger(promiseFactory: ITask>, delay?: number): Promise> { - // @ts-ignore - return super.trigger(() => { return this.throttler.queue(promiseFactory); }, delay); - } + public trigger(promiseFactory: ITask>, delay?: number): Promise> { + // @ts-ignore + return super.trigger(() => { return this.throttler.queue(promiseFactory); }, delay); + } } diff --git a/src/features/utils/lineDecoder.ts b/src/features/utils/lineDecoder.ts index a9974c6..b28fd00 100644 --- a/src/features/utils/lineDecoder.ts +++ b/src/features/utils/lineDecoder.ts @@ -1,64 +1,64 @@ import { StringDecoder } from "string_decoder"; export class LineDecoder { - private stringDecoder: StringDecoder; - private remaining: string; - private lines: string[]; + private stringDecoder: StringDecoder; + private remaining: string; + private lines: string[]; - constructor(encoding: BufferEncoding = "utf8") { - this.stringDecoder = new StringDecoder(encoding); - this.remaining = ""; - this.lines = []; - } + constructor(encoding: BufferEncoding = "utf8") { + this.stringDecoder = new StringDecoder(encoding); + this.remaining = ""; + this.lines = []; + } - public write(buffer: Buffer): string[] { - const result: string[] = []; - const value = this.remaining - ? this.remaining + this.stringDecoder.write(buffer) - : this.stringDecoder.write(buffer); + public write(buffer: Buffer): string[] { + const result: string[] = []; + const value = this.remaining + ? this.remaining + this.stringDecoder.write(buffer) + : this.stringDecoder.write(buffer); - if (value.length < 1) { - this.lines = this.lines.concat(value); - return result; - } + if (value.length < 1) { + this.lines = this.lines.concat(value); + return result; + } - let start = 0; - let characterCode: number = value.charCodeAt(start); - while (start < value.length && (characterCode === 10 || characterCode === 13)) { - start++; - } + let start = 0; + let characterCode: number = value.charCodeAt(start); + while (start < value.length && (characterCode === 10 || characterCode === 13)) { + start++; + } - let idx = start; - while (idx < value.length) { - characterCode = value.charCodeAt(idx); - if (characterCode === 10 || characterCode === 13) { - result.push(value.substring(start, idx)); - idx++; - characterCode = value.charCodeAt(idx); - while (idx < value.length && (characterCode === 10 || characterCode === 13)) { - idx++; - } - start = idx; - } else { - idx++; - } - } + let idx = start; + while (idx < value.length) { + characterCode = value.charCodeAt(idx); + if (characterCode === 10 || characterCode === 13) { + result.push(value.substring(start, idx)); + idx++; + characterCode = value.charCodeAt(idx); + while (idx < value.length && (characterCode === 10 || characterCode === 13)) { + idx++; + } + start = idx; + } else { + idx++; + } + } - this.remaining = start < value.length ? value.substring(start) : ""; - this.lines = this.lines.concat(result); + this.remaining = start < value.length ? value.substring(start) : ""; + this.lines = this.lines.concat(result); - return result; - } + return result; + } - public end(): string { - if (this.remaining && this.remaining.length > 0) { - this.lines = this.lines.concat(this.remaining); - } + public end(): string { + if (this.remaining && this.remaining.length > 0) { + this.lines = this.lines.concat(this.remaining); + } - return this.remaining; - } + return this.remaining; + } - public getLines(): string[] { - return this.lines; - } + public getLines(): string[] { + return this.lines; + } } diff --git a/src/features/utils/lintingProvider.ts b/src/features/utils/lintingProvider.ts index 10265cb..3f665df 100644 --- a/src/features/utils/lintingProvider.ts +++ b/src/features/utils/lintingProvider.ts @@ -7,173 +7,173 @@ import { ThrottledDelayer } from "./async"; import { LineDecoder } from "./lineDecoder"; enum RunTrigger { - onSave = "onSave", - onType = "onType", - off = "off" + onSave = "onSave", + onType = "onType", + off = "off" } export interface Linter { - languageId: Array, - process: (output: string[]) => vscode.Diagnostic[]; + 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.onSave) { - this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => { - this.triggerLint(e.document); - }); - } else { - this.documentListener = vscode.workspace.onDidSaveTextDocument(this.triggerLint, this); - } - 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(textDocument: vscode.TextDocument): Promise { - return new Promise((resolve) => { - const decoder = new LineDecoder(); - const executable = Configuration.executablePath(); - const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath; - - let args: string[]; - let diagnostics: vscode.Diagnostic[] = []; - - const options = rootPath ? - { - cwd: rootPath, - env: { - LANG: "en_US.utf-8" - } - } : undefined; - - if (Configuration.runTrigger() === RunTrigger.onSave) { - args = [...Configuration.lintFileArguments()]; - args.push(textDocument.fileName); - } else { - args = Configuration.lintBufferArguments(); - } - args = args.concat(Configuration.extraArguments()); - - if (this.childProcess) { - this.childProcess.kill(); - } - this.childProcess = cp.spawn(executable, args, options); - - this.childProcess.on("error", (error: Error) => { - let message = ""; - if (this.executableNotFound) { - resolve(); - return; - } - - if ((error).code === "ENOENT") { - message = `Cannot lint ${textDocument.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: ${executable}. Reason is unknown.`; - } - - this.oldExecutablePath = Configuration.executablePath(); - this.executableNotFound = true; - vscode.window.showInformationMessage(message); - resolve(); - }); - - 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(textDocument.uri, diagnostics); - resolve(); - }; - - if (this.childProcess.pid) { - if (Configuration.runTrigger() === RunTrigger.onType) { - this.childProcess.stdin.write(textDocument.getText()); - this.childProcess.stdin.end(); - } - - this.childProcess.stdout.on("data", onDataEvent); - this.childProcess.stdout.on("end", onEndEvent); - resolve(); - } else { - resolve(); - } - }); - } + 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.onSave) { + this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => { + this.triggerLint(e.document); + }); + } else { + this.documentListener = vscode.workspace.onDidSaveTextDocument(this.triggerLint, this); + } + 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(textDocument: vscode.TextDocument): Promise { + return new Promise((resolve) => { + const decoder = new LineDecoder(); + const executable = Configuration.executablePath(); + const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath; + + let args: string[]; + let diagnostics: vscode.Diagnostic[] = []; + + const options = rootPath ? + { + cwd: rootPath, + env: { + LANG: "en_US.utf-8" + } + } : undefined; + + if (Configuration.runTrigger() === RunTrigger.onSave) { + args = [...Configuration.lintFileArguments()]; + args.push(textDocument.fileName); + } else { + args = Configuration.lintBufferArguments(); + } + args = args.concat(Configuration.extraArguments()); + + if (this.childProcess) { + this.childProcess.kill(); + } + this.childProcess = cp.spawn(executable, args, options); + + this.childProcess.on("error", (error: Error) => { + let message = ""; + if (this.executableNotFound) { + resolve(); + return; + } + + if ((error).code === "ENOENT") { + message = `Cannot lint ${textDocument.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: ${executable}. Reason is unknown.`; + } + + this.oldExecutablePath = Configuration.executablePath(); + this.executableNotFound = true; + vscode.window.showInformationMessage(message); + resolve(); + }); + + 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(textDocument.uri, diagnostics); + resolve(); + }; + + if (this.childProcess.pid) { + if (Configuration.runTrigger() === RunTrigger.onType) { + this.childProcess.stdin.write(textDocument.getText()); + this.childProcess.stdin.end(); + } + + this.childProcess.stdout.on("data", onDataEvent); + this.childProcess.stdout.on("end", onEndEvent); + resolve(); + } else { + resolve(); + } + }); + } } diff --git a/test/runTest.ts b/test/runTest.ts index 36621c2..62ec431 100644 --- a/test/runTest.ts +++ b/test/runTest.ts @@ -1,23 +1,22 @@ -import * as path from 'path'; - -import { runTests } from 'vscode-test'; +import * as path from "path"; +import { runTests } from "vscode-test"; const main = async () => { - try { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); - // The path to test runner - // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, './suite/index'); + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath }); - } catch (err) { - console.error('Failed to run tests'); - process.exit(1); - } + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error("Failed to run tests"); + process.exit(1); + } }; main(); diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 1856ed7..4873a6f 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -1,68 +1,68 @@ -import * as assert from 'assert'; +import * as assert from "assert"; +import * as vscode from "vscode"; -import * as vscode from 'vscode'; -import { activate, getDocUri, toRange, format } from './helper'; +import { activate, format, getDocUri, toRange } from "./helper"; const TIMEOUT = 4000; -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); +suite("Extension Test Suite", () => { + vscode.window.showInformationMessage("Start all tests."); - test('Good SQL should have zero diagnostics', async () => { + test("Good SQL should have zero diagnostics", async () => { - const docUri = getDocUri('/test_sql/good.sql'); - await activate(docUri); + const docUri = getDocUri("/test_sql/good.sql"); + await activate(docUri); - const actualDiagnostics = vscode.languages.getDiagnostics(docUri); + const actualDiagnostics = vscode.languages.getDiagnostics(docUri); - assert.strictEqual(actualDiagnostics.length, 0); + assert.strictEqual(actualDiagnostics.length, 0); - }).timeout(TIMEOUT); + }).timeout(TIMEOUT); - test('Bad SQL should have the correct diagnostics', async () => { + test("Bad SQL should have the correct diagnostics", async () => { - const docUri = getDocUri('/test_sql/bad.sql'); - await activate(docUri); + const docUri = getDocUri("/test_sql/bad.sql"); + await activate(docUri); - const actualDiagnostics = vscode.languages.getDiagnostics(docUri); + const actualDiagnostics = vscode.languages.getDiagnostics(docUri); - assert.strictEqual(actualDiagnostics.length, 3); - [ - { range: toRange(0, 1, 0, 1), message: "Query produces an unknown number of result columns.", code: "L044" }, - { range: toRange(0, 10, 0, 10), message: "Inconsistent capitalisation of keywords.", code: "L010" }, - { range: toRange(0, 22, 0, 22), message: "Files must end with a trailing newline.", code: "L009" }, - ].forEach((expectedDiagnostic, i) => { - assertDiagnosticIsEqual(actualDiagnostics[i], expectedDiagnostic); - }); + assert.strictEqual(actualDiagnostics.length, 3); + [ + { range: toRange(0, 1, 0, 1), message: "Query produces an unknown number of result columns.", code: "L044" }, + { range: toRange(0, 10, 0, 10), message: "Inconsistent capitalisation of keywords.", code: "L010" }, + { range: toRange(0, 22, 0, 22), message: "Files must end with a trailing newline.", code: "L009" }, + ].forEach((expectedDiagnostic, i) => { + assertDiagnosticIsEqual(actualDiagnostics[i], expectedDiagnostic); + }); - }).timeout(TIMEOUT); + }).timeout(TIMEOUT); - test("Bad SQL has zero diagnostics after document format", async () => { + test("Bad SQL has zero diagnostics after document format", async () => { - const docUri = getDocUri('/test_sql/format.sql'); - const document = await activate(docUri); - const preFormatDiagnostics = vscode.languages.getDiagnostics(docUri); - assert.strictEqual(preFormatDiagnostics.length, 1, "Pre-format diagnostics not expected length"); + const docUri = getDocUri("/test_sql/format.sql"); + const document = await activate(docUri); + const preFormatDiagnostics = vscode.languages.getDiagnostics(docUri); + assert.strictEqual(preFormatDiagnostics.length, 1, "Pre-format diagnostics not expected length"); - await format(document); - await activate(docUri); + await format(document); + await activate(docUri); - const postFormatDiagnostics = vscode.languages.getDiagnostics(docUri); - assert.strictEqual(postFormatDiagnostics.length, 0, "Post-format diagnostics not expected length"); + const postFormatDiagnostics = vscode.languages.getDiagnostics(docUri); + assert.strictEqual(postFormatDiagnostics.length, 0, "Post-format diagnostics not expected length"); - }).timeout(9999999); + }).timeout(9999999); - const assertDiagnosticIsEqual = (actual: vscode.Diagnostic, expected: { range: any; message: any; code: any; }) => { - assert.deepStrictEqual(actual.range, expected.range); - assert.strictEqual(actual.severity, 0); - assert.strictEqual(actual.message, expected.message); - assert.strictEqual(actual.code, expected.code); - assert.strictEqual(actual.source, "sqlfluff"); - }; + const assertDiagnosticIsEqual = (actual: vscode.Diagnostic, expected: { range: any; message: any; code: any; }) => { + assert.deepStrictEqual(actual.range, expected.range); + assert.strictEqual(actual.severity, 0); + assert.strictEqual(actual.message, expected.message); + assert.strictEqual(actual.code, expected.code); + assert.strictEqual(actual.source, "sqlfluff"); + }; }); diff --git a/test/suite/helper.ts b/test/suite/helper.ts index 8edccf9..2ff61cd 100644 --- a/test/suite/helper.ts +++ b/test/suite/helper.ts @@ -1,8 +1,8 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; export const activate = async (docUri: vscode.Uri): Promise => { // The extensionId is `publisher.name` from package.json - const ext = vscode.extensions.getExtension('vscode-sqlfluff'); + const ext = vscode.extensions.getExtension("vscode-sqlfluff"); await ext?.activate(); try { const document = await vscode.workspace.openTextDocument(docUri); @@ -22,7 +22,7 @@ export const format = async (document: vscode.TextDocument | undefined) => { await vscode.commands.executeCommand("editor.action.formatDocument"); await sleep(2000); await document.save(); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); } catch (e) { console.error(e); } @@ -43,4 +43,4 @@ export const toRange = (sLine: number, sChar: number, eLine: number, eChar: numb const start = new vscode.Position(sLine, sChar); const end = new vscode.Position(eLine, eChar); return new vscode.Range(start, end); -}; \ No newline at end of file +}; diff --git a/test/suite/index.ts b/test/suite/index.ts index 613e8ff..453a603 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -1,38 +1,38 @@ -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import * as glob from "glob"; +import * as Mocha from "mocha"; +import * as path from "path"; export const run = (): Promise => { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd', - color: true - }); + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + color: true + }); - const testsRoot = path.resolve(__dirname, '..'); + const testsRoot = path.resolve(__dirname, ".."); - return new Promise((c, e) => { - glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } - // Add files to the test suite - files.forEach(f => {return mocha.addFile(path.resolve(testsRoot, f));}); + // Add files to the test suite + files.forEach(f => { return mocha.addFile(path.resolve(testsRoot, f)); }); - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } - }); - }); + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); }; diff --git a/test/suite/test_sql/bad.sql b/test/suite/test_sql/bad.sql index 8df055f..f3052e8 100644 --- a/test/suite/test_sql/bad.sql +++ b/test/suite/test_sql/bad.sql @@ -1,2 +1,2 @@ -- 割引金額 -select a from b; \ No newline at end of file +select a from b; diff --git a/test/suite/test_sql/format.sql b/test/suite/test_sql/format.sql index f411e70..c5fd447 100644 --- a/test/suite/test_sql/format.sql +++ b/test/suite/test_sql/format.sql @@ -1 +1 @@ -SELECT a FROM a_table; \ No newline at end of file +SELECT a FROM a_table;