| // Copyright 2022 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| import * as childProcess from 'child_process'; |
| import * as vscode from 'vscode'; |
| |
| export function activate(context: vscode.ExtensionContext) { |
| const collection = vscode.languages.createDiagnosticCollection('cros-lint'); |
| if (vscode.window.activeTextEditor) { |
| updateCrosLintDiagnostics( |
| vscode.window.activeTextEditor.document, collection); |
| } |
| context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor( |
| editor => { |
| if (editor) { |
| updateCrosLintDiagnostics(editor.document, collection); |
| } |
| })); |
| context.subscriptions.push(vscode.workspace.onDidSaveTextDocument( |
| document => { |
| updateCrosLintDiagnostics(document, collection); |
| })); |
| context.subscriptions.push(vscode.workspace.onDidCloseTextDocument( |
| document => { |
| collection.delete(document.uri); |
| })); |
| } |
| |
| // Describes how to run a linter and parse its output. |
| interface LintConfig { |
| command(path: string) : string; |
| parse(stdout: string, stderr: string, document: vscode.TextDocument) |
| : vscode.Diagnostic[]; |
| } |
| |
| const GNLINT_PATH = '~/chromiumos/src/platform2/common-mk/gnlint.py'; |
| |
| // Don't forget to update package.json when adding more languages. |
| const lintConfigs = new Map<string, LintConfig>([ |
| ['cpp', { |
| command: (path: string) => `cros lint ${path}`, |
| parse: parseCrosLintCpp, |
| }], |
| ['gn', { |
| command: (path: string) => GNLINT_PATH + ` ${path}`, |
| parse: parseCrosLintGn, |
| }], |
| ['python', { |
| command: (path: string) => `cros lint ${path}`, |
| parse: parseCrosLintShellPython, |
| }], |
| ['shellscript', { |
| command: (path: string) => `cros lint --output=parseable ${path}`, |
| parse: parseCrosLintShellPython, |
| }], |
| ]); |
| |
| function updateCrosLintDiagnostics( |
| document: vscode.TextDocument, |
| collection: vscode.DiagnosticCollection): void { |
| if (document && document.uri.scheme === 'file') { |
| const lintConfig = lintConfigs.get(document.languageId); |
| if (lintConfig) { |
| childProcess.exec(lintConfig.command(document.uri.fsPath), |
| (_error, stdout, stderr) => { |
| const diagnostics = lintConfig.parse(stdout, stderr, document); |
| collection.set(document.uri, diagnostics); |
| }); |
| } |
| } else { |
| collection.clear(); |
| } |
| } |
| |
| export function parseCrosLintCpp( |
| stdout: string, stderr: string, document: vscode.TextDocument) |
| :vscode.Diagnostic[] { |
| const lineRE = /^([^ \n]+):([0-9]+): (.*) \[([^ ]+)\] \[([1-5])\]/gm; |
| const diagnostics: vscode.Diagnostic[] = []; |
| let match: RegExpExecArray | null; |
| // stdout and stderr are merged, because we saw that warnings can go to |
| // either. |
| // TODO(b/214322467): Figure out when we should use stderr and when stdout. |
| while ((match = lineRE.exec(stdout + '\n' + stderr)) !== null) { |
| const file = match[1]; |
| let line = Number(match[2]); |
| // Warning about missing copyright is reported at hard coded line 0. |
| // This seems like a bug in cpplint.py, which otherwise uses 1-based |
| // line numbers. |
| if (line === 0) { |
| line = 1; |
| } |
| const message = match[3]; |
| if (file === document.uri.fsPath) { |
| diagnostics.push(createDiagnostic(message, line)); |
| } |
| } |
| return diagnostics; |
| } |
| |
| // Parse output from platform2/common-mk/gnlint.py on a GN file. |
| export function parseCrosLintGn(_stdout: string, stderr: string, document: vscode.TextDocument) |
| : vscode.Diagnostic[] { |
| // Only the errors that have location in the file are captured. |
| // There are two categories of errors without line/column number: |
| // - file not formatted by gn-format: should do auto-format upon save |
| // - wrong commandline arguments: should be covered by extension unit test |
| // So these are ignored. |
| const lineRE = /ERROR: ([^ \n\:]+):([0-9]+):([0-9]+): (.*)/gm; |
| const diagnostics: vscode.Diagnostic[] = []; |
| let match: RegExpExecArray | null; |
| while ((match = lineRE.exec(stderr)) !== null) { |
| const file = match[1]; |
| const line = Number(match[2]); |
| const startCol = Number(match[3]); |
| const message = match[4]; |
| if (file === document.uri.fsPath) { |
| diagnostics.push(createDiagnostic(message, line, startCol)); |
| } |
| } |
| return diagnostics; |
| } |
| |
| // Parse output from cros lint on Python files |
| // or cros lint --output=parseable on shell files. |
| // TODO(b/214322467): Add unit tests for Python and shell. |
| export function parseCrosLintShellPython( |
| stdout: string, _stderr: string, document: vscode.TextDocument) |
| : vscode.Diagnostic[] { |
| const lineRE = /^([^ \n\:]+):([0-9]+):([0-9]+): (.*)/gm; |
| const diagnostics: vscode.Diagnostic[] = []; |
| let match: RegExpExecArray | null; |
| while ((match = lineRE.exec(stdout)) !== null) { |
| const file = match[1]; |
| const line = Number(match[2]); |
| const startCol = Number(match[3]); |
| const message = match[4]; |
| if (file === document.uri.fsPath) { |
| diagnostics.push(createDiagnostic(message, line, startCol)); |
| } |
| } |
| return diagnostics; |
| } |
| |
| function createDiagnostic(message : string, line: number, startCol?: number) |
| : vscode.Diagnostic { |
| return new vscode.Diagnostic( |
| new vscode.Range( |
| new vscode.Position(line - 1, startCol ? startCol : 0), |
| new vscode.Position(line - 1, Number.MAX_VALUE), |
| ), |
| message, |
| // TODO(b/214322467): Should these actually be errors when they block |
| // repo upload? |
| vscode.DiagnosticSeverity.Warning, |
| ); |
| } |