diff --git a/README.md b/README.md index c49a51d..afec4ce 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,13 @@ - **On-save linting**: When you save a c/cpp file, `cppcheck` is automatically run on that file. - **Per-file diagnostics**: Only diagnostics relevant to the saved file are displayed. - **Configurable severity threshold**: Filter out messages below a chosen severity level (`info`, `warning`, or `error`). -- **Set C/C++ standard**: Easily specify `--std=` (e.g. `c++17`, `c99`, etc.). - **Diagnostic cleanup**: When you close a file, its diagnostics are automatically cleared. - **Project file support**: You can feed your project file to cppcheck through the `--project` flag in the `cppcheck-official.arguments` field in the extension settings. - **Warning notes**: Display notes for warnings when those are available - +- **Dynamic config**: The extension supports running a script to generate arguments to pass to cppcheck. This can be done by including the command in the argument field wrapped with \${}, e.g. `--suppress=memleak:src/file1.cpp ${bash path/to/script.sh}`. The script is expected to output the argument(s) wrapped with \${}. If the script e.g. creates a project file it should print out as `${--project=path/to/projectfile.json}`. This output will be spliced into the argument string as such: `--suppress=memleak:src/file1.cpp --project=path/to/projectfile.json`. ## Requirements - **Cppcheck** must be installed on your system. + **Cppcheck** must be installed on your system. - By default, this extension looks for `cppcheck` on the system PATH. - Alternatively, specify a custom executable path using the `cppcheck-official.path` setting. diff --git a/src/extension.ts b/src/extension.ts index 1bd1188..51ed09b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,9 +1,11 @@ import * as vscode from 'vscode'; import * as cp from 'child_process'; import * as path from "path"; -import * as os from "os"; import * as xml2js from 'xml2js'; +import { runCommand } from './util/scripts'; +import { resolvePath } from './util/path'; + enum SeverityNumber { Info = 0, Warning = 1, @@ -54,42 +56,14 @@ function parseMinSeverity(str: string): SeverityNumber { } } -export function resolvePath(argPath: string): string { - const folders = vscode.workspace.workspaceFolders; - const workspaceRoot = folders && folders.length > 0 - ? folders[0].uri.fsPath - : process.cwd(); - - // Expand ${workspaceFolder} - if (argPath.includes("${workspaceFolder}")) { - argPath = argPath.replace("${workspaceFolder}", workspaceRoot); - } - - // Expand tilde (~) to home directory - if (argPath.startsWith("~")) { - argPath = path.join(os.homedir(), argPath.slice(1)); - } - - // Expand ./ or ../ relative paths (relative to workspace root if available) - if (argPath.startsWith("./") || argPath.startsWith("../")) { - argPath = path.resolve(workspaceRoot, argPath); - } - - // If still not absolute, treat it as relative to workspace root - if (!path.isAbsolute(argPath)) { - argPath = path.join(workspaceRoot, argPath); - } - return argPath; -} - // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { // Create a diagnostic collection. const diagnosticCollection = vscode.languages.createDiagnosticCollection("Cppcheck"); context.subscriptions.push(diagnosticCollection); - + // set up a map of timers per document URI for debounce for continuous analysis triggers // I.e. document has been changed -> DEBOUNCE_MS time passed since last change -> run cppcheck const debounceTimers: Map = new Map(); @@ -112,11 +86,23 @@ export function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration(); const isEnabled = config.get("cppcheck-official.enable", true); - const extraArgs = config.get("cppcheck-official.arguments", ""); const minSevString = config.get("cppcheck-official.minSeverity", "info"); const userPath = config.get("cppcheck-official.path")?.trim() || ""; const commandPath = userPath ? resolvePath(userPath) : "cppcheck"; + var args = config.get("cppcheck-official.arguments", ""); + var processedArgs = ''; + // If argument field contains command to run script we do so here + if (args.includes('${')) { + const scriptCommand = args.split("${")[1].split("}")[0]; + const scriptOutput = await runCommand(scriptCommand); + // We expect that the script output that is to be used as arguments will be wrapped with ${} + const scriptOutputTrimmed = scriptOutput.split("${")[1].split("}")[0]; + processedArgs = args.split("${")[0] + scriptOutputTrimmed + args.split("}")?.[1]; + } else { + processedArgs = args; + } + // If disabled, clear any existing diagnostics for this doc. if (!isEnabled) { diagnosticCollection.delete(document.uri); @@ -137,7 +123,7 @@ export function activate(context: vscode.ExtensionContext) { await runCppcheckOnFileXML( document, commandPath, - extraArgs, + processedArgs, minSevString, diagnosticCollection ); @@ -187,7 +173,7 @@ export function activate(context: vscode.ExtensionContext) { async function runCppcheckOnFileXML( document: vscode.TextDocument, commandPath: string, - extraArgs: string, + processedArgs: string, minSevString: string, diagnosticCollection: vscode.DiagnosticCollection ): Promise { @@ -199,7 +185,7 @@ async function runCppcheckOnFileXML( const minSevNum = parseMinSeverity(minSevString); // Resolve paths for arguments where applicable - const extraArgsParsed = (extraArgs.split(" ")).map((arg) => { + const argsParsed = processedArgs.split(" ").map((arg) => { if (arg.startsWith('--project')) { const splitArg = arg.split('='); return `${splitArg[0]}=${resolvePath(splitArg[1])}`; @@ -208,7 +194,7 @@ async function runCppcheckOnFileXML( }); let proc; - if (extraArgs.includes("--project")) { + if (processedArgs.includes("--project")) { const args = [ '--enable=all', '--inline-suppr', @@ -217,7 +203,7 @@ async function runCppcheckOnFileXML( '--suppress=missingInclude', '--suppress=missingIncludeSystem', `--file-filter=${filePath}`, - ...extraArgsParsed, + ...argsParsed, ].filter(Boolean); proc = cp.spawn(commandPath, args, { cwd: path.dirname(document.fileName), @@ -230,7 +216,7 @@ async function runCppcheckOnFileXML( '--suppress=unusedFunction', '--suppress=missingInclude', '--suppress=missingIncludeSystem', - ...extraArgsParsed, + ...argsParsed, filePath, ].filter(Boolean); proc = cp.spawn(commandPath, args, { diff --git a/src/util/path.ts b/src/util/path.ts new file mode 100644 index 0000000..2ca36ff --- /dev/null +++ b/src/util/path.ts @@ -0,0 +1,31 @@ +import * as path from "path"; +import * as os from "os"; +import * as vscode from 'vscode'; + +export function resolvePath(argPath: string): string { + const folders = vscode.workspace.workspaceFolders; + const workspaceRoot = folders && folders.length > 0 + ? folders[0].uri.fsPath + : process.cwd(); + + // Expand ${workspaceFolder} + if (argPath.includes("${workspaceFolder}")) { + argPath = argPath.replace("${workspaceFolder}", workspaceRoot); + } + + // Expand tilde (~) to home directory + if (argPath.startsWith("~")) { + argPath = path.join(os.homedir(), argPath.slice(1)); + } + + // Expand ./ or ../ relative paths (relative to workspace root if available) + if (argPath.startsWith("./") || argPath.startsWith("../")) { + argPath = path.resolve(workspaceRoot, argPath); + } + + // If still not absolute, treat it as relative to workspace root + if (!path.isAbsolute(argPath)) { + argPath = path.join(workspaceRoot, argPath); + } + return argPath; +} \ No newline at end of file diff --git a/src/util/scripts.ts b/src/util/scripts.ts new file mode 100644 index 0000000..54d160a --- /dev/null +++ b/src/util/scripts.ts @@ -0,0 +1,22 @@ +import { exec } from "child_process"; +import { resolvePath } from './path'; +import util from 'util'; + +const execAsync = util.promisify(exec); + +async function runCommand(command : string) { + try { + const { stdout, stderr } = await execAsync(command, { + cwd: resolvePath('${workspaceFolder}'), + }); + + if (stderr) { + throw new Error(stderr); + } + return stdout; + } catch (error) { + throw error; + } +} + +export { runCommand }; \ No newline at end of file