diff --git a/Extension/.gitignore b/Extension/.gitignore index 1adad30d0..1965b6766 100644 --- a/Extension/.gitignore +++ b/Extension/.gitignore @@ -10,6 +10,7 @@ server debugAdapters LLVM bin/cpptools* +bin/libc.so bin/*.dll bin/.vs bin/LICENSE.txt diff --git a/Extension/c_cpp_properties.schema.json b/Extension/c_cpp_properties.schema.json index 939cb8a34..b3ac9a6d4 100644 --- a/Extension/c_cpp_properties.schema.json +++ b/Extension/c_cpp_properties.schema.json @@ -19,8 +19,8 @@ "markdownDescription": "Full path of the compiler being used, e.g. `/usr/bin/gcc`, to enable more accurate IntelliSense.", "descriptionHint": "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered.", "type": [ - "string", - "null" + "null", + "string" ] }, "compilerArgs": { @@ -312,4 +312,4 @@ "version" ], "additionalProperties": false -} +} \ No newline at end of file diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index 26bbd86ec..406ae4369 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -63,7 +63,7 @@ export interface ConfigurationJson { export interface Configuration { name: string; compilerPathInCppPropertiesJson?: string | null; - compilerPath?: string | null; + compilerPath?: string; // Can be set to null based on the schema, but it will be fixed in parsePropertiesFile. compilerPathIsExplicit?: boolean; compilerArgs?: string[]; compilerArgsLegacy?: string[]; @@ -982,14 +982,13 @@ export class CppProperties { } else { // However, if compileCommands are used and compilerPath is explicitly set, it's still necessary to resolve variables in it. if (configuration.compilerPath === "${default}") { - configuration.compilerPath = settings.defaultCompilerPath; - } - if (configuration.compilerPath === null) { + configuration.compilerPath = settings.defaultCompilerPath ?? undefined; configuration.compilerPathIsExplicit = true; - } else if (configuration.compilerPath !== undefined) { + } + if (configuration.compilerPath) { configuration.compilerPath = util.resolveVariables(configuration.compilerPath, env); configuration.compilerPathIsExplicit = true; - } else { + } else if (configuration.compilerPathIsExplicit === undefined) { configuration.compilerPathIsExplicit = false; } } @@ -1444,10 +1443,17 @@ export class CppProperties { } } - // Configuration.compileCommands is allowed to be defined as a string in the schema, but we send an array to the language server. - // For having a predictable behavior, we convert it here to an array of strings. + // Special sanitization of the newly parsed configuration file happens here: for (let i: number = 0; i < newJson.configurations.length; i++) { + // Configuration.compileCommands is allowed to be defined as a string in the schema, but we send an array to the language server. + // For having a predictable behavior, we convert it here to an array of strings. newJson.configurations[i].compileCommands = this.forceCompileCommandsAsArray(newJson.configurations[i].compileCommands); + + // `compilerPath` is allowed to be set to null in the schema so that empty string is not the default value (which has another meaning). + // If we detect this, we treat it as undefined. + if (newJson.configurations[i].compilerPath === null) { + delete newJson.configurations[i].compilerPath; + } } this.configurationJson = newJson; @@ -1596,92 +1602,97 @@ export class CppProperties { return result; } - private getErrorsForConfigUI(configIndex: number): ConfigurationErrors { - const errors: ConfigurationErrors = {}; - if (!this.configurationJson) { - return errors; - } - const isWindows: boolean = os.platform() === 'win32'; - const config: Configuration = this.configurationJson.configurations[configIndex]; - - // Check if config name is unique. - errors.name = this.isConfigNameUnique(config.name); - let resolvedCompilerPath: string | undefined | null; - // Validate compilerPath - if (config.compilerPath) { - resolvedCompilerPath = which.sync(config.compilerPath, { nothrow: true }); - } - + /** + * Get the compilerPath and args from a compilerPath string that has already passed through + * `this.resolvePath`. If there are errors processing the path, those will also be returned. + * + * @param resolvedCompilerPath a compilerPath string that has already been resolved. + * @param rootUri the workspace folder URI, if any. + */ + public static validateCompilerPath(resolvedCompilerPath: string, rootUri?: vscode.Uri): util.CompilerPathAndArgs { if (!resolvedCompilerPath) { - resolvedCompilerPath = this.resolvePath(config.compilerPath); + return { compilerName: '', allCompilerArgs: [], compilerArgsFromCommandLineInPath: [] }; } - const settings: CppSettings = new CppSettings(this.rootUri); - const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, resolvedCompilerPath); + resolvedCompilerPath = resolvedCompilerPath.trim(); - // compilerPath + args in the same string isn't working yet. - const skipFullCommandString = !compilerPathAndArgs.compilerName && resolvedCompilerPath.includes(" "); - if (resolvedCompilerPath - && !skipFullCommandString - // Don't error cl.exe paths because it could be for an older preview build. - && compilerPathAndArgs.compilerName.toLowerCase() !== "cl.exe" - && compilerPathAndArgs.compilerName.toLowerCase() !== "cl") { - resolvedCompilerPath = resolvedCompilerPath.trim(); - - // Error when the compiler's path has spaces without quotes but args are used. - // Except, exclude cl.exe paths because it could be for an older preview build. - const compilerPathNeedsQuotes: boolean = - (compilerPathAndArgs.compilerArgsFromCommandLineInPath && compilerPathAndArgs.compilerArgsFromCommandLineInPath.length > 0) && - !resolvedCompilerPath.startsWith('"') && - compilerPathAndArgs.compilerPath !== undefined && - compilerPathAndArgs.compilerPath !== null && - compilerPathAndArgs.compilerPath.includes(" "); + const settings = new CppSettings(rootUri); + const compilerPathAndArgs = util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, resolvedCompilerPath, undefined, rootUri?.fsPath); + const compilerLowerCase: string = compilerPathAndArgs.compilerName.toLowerCase(); + const isCl: boolean = compilerLowerCase === "cl" || compilerLowerCase === "cl.exe"; + const telemetry: { [key: string]: number } = {}; - const compilerPathErrors: string[] = []; - if (compilerPathNeedsQuotes) { - compilerPathErrors.push(localize("path.with.spaces", 'Compiler path with spaces and arguments is missing double quotes " around the path.')); - } - - // Get compiler path without arguments before checking if it exists - resolvedCompilerPath = compilerPathAndArgs.compilerPath ?? undefined; - if (resolvedCompilerPath) { - let pathExists: boolean = true; - const existsWithExeAdded: (path: string) => boolean = (path: string) => isWindows && !path.startsWith("/") && fs.existsSync(path + ".exe"); - if (!fs.existsSync(resolvedCompilerPath)) { - if (existsWithExeAdded(resolvedCompilerPath)) { - resolvedCompilerPath += ".exe"; - } else if (!this.rootUri) { - pathExists = false; - } else { - // Check again for a relative path. - const relativePath: string = this.rootUri.fsPath + path.sep + resolvedCompilerPath; - if (!fs.existsSync(relativePath)) { - if (existsWithExeAdded(resolvedCompilerPath)) { - resolvedCompilerPath += ".exe"; + // Don't error cl.exe paths because it could be for an older preview build. + if (!isCl && compilerPathAndArgs.compilerPath) { + const compilerPathMayNeedQuotes: boolean = !resolvedCompilerPath.startsWith('"') && resolvedCompilerPath.includes(" ") && compilerPathAndArgs.compilerArgsFromCommandLineInPath.length > 0; + let pathExists: boolean = true; + const existsWithExeAdded: (path: string) => boolean = (path: string) => isWindows && !path.startsWith("/") && fs.existsSync(path + ".exe"); + + resolvedCompilerPath = compilerPathAndArgs.compilerPath; + if (!fs.existsSync(resolvedCompilerPath)) { + if (existsWithExeAdded(resolvedCompilerPath)) { + resolvedCompilerPath += ".exe"; + } else { + const pathLocation = which.sync(resolvedCompilerPath, { nothrow: true }); + if (pathLocation) { + resolvedCompilerPath = pathLocation; + compilerPathAndArgs.compilerPath = pathLocation; + } else if (rootUri) { + // Test if it was a relative path. + const absolutePath: string = rootUri.fsPath + path.sep + resolvedCompilerPath; + if (!fs.existsSync(absolutePath)) { + if (existsWithExeAdded(absolutePath)) { + resolvedCompilerPath = absolutePath + ".exe"; } else { pathExists = false; } } else { - resolvedCompilerPath = relativePath; + resolvedCompilerPath = absolutePath; } } } + } - if (!pathExists) { - const message: string = localize('cannot.find', "Cannot find: {0}", resolvedCompilerPath); - compilerPathErrors.push(message); - } else if (compilerPathAndArgs.compilerPath === "") { - const message: string = localize("cannot.resolve.compiler.path", "Invalid input, cannot resolve compiler path"); - compilerPathErrors.push(message); - } else if (!util.checkExecutableWithoutExtensionExistsSync(resolvedCompilerPath)) { - const message: string = localize("path.is.not.a.file", "Path is not a file: {0}", resolvedCompilerPath); - compilerPathErrors.push(message); - } + const compilerPathErrors: string[] = []; + if (compilerPathMayNeedQuotes && !pathExists) { + compilerPathErrors.push(localize("path.with.spaces", 'Compiler path with spaces could not be found. If this was intended to include compiler arguments, surround the compiler path with double quotes (").')); + telemetry.CompilerPathMissingQuotes = 1; + } - if (compilerPathErrors.length > 0) { - errors.compilerPath = compilerPathErrors.join('\n'); - } + if (!pathExists) { + const message: string = localize('cannot.find', "Cannot find: {0}", resolvedCompilerPath); + compilerPathErrors.push(message); + telemetry.PathNonExistent = 1; + } else if (!util.checkExecutableWithoutExtensionExistsSync(resolvedCompilerPath)) { + const message: string = localize("path.is.not.a.file", "Path is not a file: {0}", resolvedCompilerPath); + compilerPathErrors.push(message); + telemetry.PathNotAFile = 1; } + + if (compilerPathErrors.length > 0) { + compilerPathAndArgs.error = compilerPathErrors.join('\n'); + } + } + compilerPathAndArgs.telemetry = telemetry; + return compilerPathAndArgs; + } + + private getErrorsForConfigUI(configIndex: number): ConfigurationErrors { + const errors: ConfigurationErrors = {}; + if (!this.configurationJson) { + return errors; + } + const isWindows: boolean = os.platform() === 'win32'; + const config: Configuration = this.configurationJson.configurations[configIndex]; + + // Check if config name is unique. + errors.name = this.isConfigNameUnique(config.name); + let resolvedCompilerPath: string | undefined | null; + // Validate compilerPath + if (!resolvedCompilerPath) { + resolvedCompilerPath = this.resolvePath(config.compilerPath, false, false); } + const compilerPathAndArgs: util.CompilerPathAndArgs = CppProperties.validateCompilerPath(resolvedCompilerPath, this.rootUri); + errors.compilerPath = compilerPathAndArgs.error; // Validate paths (directories) errors.includePath = this.validatePath(config.includePath, { globPaths: true }); @@ -1932,7 +1943,6 @@ export class CppProperties { // Check for path-related squiggles. const paths: string[] = []; - let compilerPath: string | undefined; for (const pathArray of [currentConfiguration.browse ? currentConfiguration.browse.path : undefined, currentConfiguration.includePath, currentConfiguration.macFrameworkPath]) { if (pathArray) { for (const curPath of pathArray) { @@ -1954,13 +1964,6 @@ export class CppProperties { paths.push(`${file}`); }); - if (currentConfiguration.compilerPath) { - // Unlike other cases, compilerPath may not start or end with " due to trimming of whitespace and the possibility of compiler args. - compilerPath = currentConfiguration.compilerPath; - } - - compilerPath = this.resolvePath(compilerPath).trim(); - // Get the start/end for properties that are file-only. const forcedIncludeStart: number = curText.search(/\s*\"forcedInclude\"\s*:\s*\[/); const forcedeIncludeEnd: number = forcedIncludeStart === -1 ? -1 : curText.indexOf("]", forcedIncludeStart); @@ -1977,46 +1980,20 @@ export class CppProperties { const processedPaths: Set = new Set(); // Validate compiler paths - let compilerPathNeedsQuotes: boolean = false; - let compilerMessage: string | undefined; - const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, compilerPath); - const compilerLowerCase: string = compilerPathAndArgs.compilerName.toLowerCase(); - const isClCompiler: boolean = compilerLowerCase === "cl" || compilerLowerCase === "cl.exe"; - // Don't squiggle for invalid cl and cl.exe paths. - if (compilerPathAndArgs.compilerPath && !isClCompiler) { - // Squiggle when the compiler's path has spaces without quotes but args are used. - compilerPathNeedsQuotes = (compilerPathAndArgs.compilerArgsFromCommandLineInPath && compilerPathAndArgs.compilerArgsFromCommandLineInPath.length > 0) - && !compilerPath.startsWith('"') - && compilerPathAndArgs.compilerPath.includes(" "); - compilerPath = compilerPathAndArgs.compilerPath; - // Don't squiggle if compiler path is resolving with environment path. - if (compilerPathNeedsQuotes || (compilerPath && !which.sync(compilerPath, { nothrow: true }))) { - if (compilerPathNeedsQuotes) { - compilerMessage = localize("path.with.spaces", 'Compiler path with spaces and arguments is missing double quotes " around the path.'); - newSquiggleMetrics.CompilerPathMissingQuotes++; - } else if (!util.checkExecutableWithoutExtensionExistsSync(compilerPath)) { - compilerMessage = localize("path.is.not.a.file", "Path is not a file: {0}", compilerPath); - newSquiggleMetrics.PathNotAFile++; - } - } - } - let compilerPathExists: boolean = true; - if (this.rootUri && !isClCompiler) { - const checkPathExists: any = util.checkPathExistsSync(compilerPath, this.rootUri.fsPath + path.sep, isWindows, true); - compilerPathExists = checkPathExists.pathExists; - compilerPath = checkPathExists.path; - } - if (!compilerPathExists) { - compilerMessage = localize('cannot.find', "Cannot find: {0}", compilerPath); - newSquiggleMetrics.PathNonExistent++; - } - if (compilerMessage) { + const resolvedCompilerPath = this.resolvePath(currentConfiguration.compilerPath, false, false); + const compilerPathAndArgs: util.CompilerPathAndArgs = CppProperties.validateCompilerPath(resolvedCompilerPath, this.rootUri); + if (compilerPathAndArgs.error) { const diagnostic: vscode.Diagnostic = new vscode.Diagnostic( - new vscode.Range(document.positionAt(curTextStartOffset + compilerPathValueStart), - document.positionAt(curTextStartOffset + compilerPathEnd)), - compilerMessage, vscode.DiagnosticSeverity.Warning); + new vscode.Range(document.positionAt(curTextStartOffset + compilerPathValueStart), document.positionAt(curTextStartOffset + compilerPathEnd)), + compilerPathAndArgs.error, + vscode.DiagnosticSeverity.Warning); diagnostics.push(diagnostic); } + if (compilerPathAndArgs.telemetry) { + for (const o of Object.keys(compilerPathAndArgs.telemetry)) { + newSquiggleMetrics[o] = compilerPathAndArgs.telemetry[o]; + } + } // validate .config path let dotConfigPath: string | undefined; diff --git a/Extension/src/LanguageServer/cppBuildTaskProvider.ts b/Extension/src/LanguageServer/cppBuildTaskProvider.ts index 014d2316c..de08d6e43 100644 --- a/Extension/src/LanguageServer/cppBuildTaskProvider.ts +++ b/Extension/src/LanguageServer/cppBuildTaskProvider.ts @@ -98,7 +98,7 @@ export class CppBuildTaskProvider implements TaskProvider { // Get user compiler path. const userCompilerPathAndArgs: util.CompilerPathAndArgs | undefined = await activeClient.getCurrentCompilerPathAndArgs(); - let userCompilerPath: string | undefined | null; + let userCompilerPath: string | undefined; if (userCompilerPathAndArgs) { userCompilerPath = userCompilerPathAndArgs.compilerPath; if (userCompilerPath && userCompilerPathAndArgs.compilerName) { diff --git a/Extension/src/common.ts b/Extension/src/common.ts index fba6c3ac3..a48326eae 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -1092,74 +1092,66 @@ export function isCl(compilerPath: string): boolean { /** CompilerPathAndArgs retains original casing of text input for compiler path and args */ export interface CompilerPathAndArgs { - compilerPath?: string | null; + compilerPath?: string; compilerName: string; compilerArgs?: string[]; compilerArgsFromCommandLineInPath: string[]; allCompilerArgs: string[]; + error?: string; + telemetry?: { [key: string]: number }; } -export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string | null, compilerArgs?: string[]): CompilerPathAndArgs { - let compilerPath: string | undefined | null = inputCompilerPath; +/** + * Parse the compiler path input into a compiler path and compiler args. If there are no args in the input string, this function will have + * verified that the compiler exists. (e.g. `compilerArgsFromCommandLineInPath` will be empty) + * + * @param useLegacyBehavior - If true, use the legacy behavior of separating the compilerPath from the args. + * @param inputCompilerPath - The compiler path input from the user. + * @param compilerArgs - The compiler args input from the user. + * @param cwd - The directory used to resolve relative paths. + */ +export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string, compilerArgs?: string[], cwd?: string): CompilerPathAndArgs { + let compilerPath: string | undefined = inputCompilerPath; let compilerName: string = ""; let compilerArgsFromCommandLineInPath: string[] = []; + const trimLegacyQuotes = (compilerPath?: string): string | undefined => { + if (compilerPath && useLegacyBehavior) { + // Try to trim quotes from compiler path. + const tempCompilerPath: string[] = extractArgs(compilerPath); + if (tempCompilerPath.length > 0) { + return tempCompilerPath[0]; + } + } + return compilerPath; + }; if (compilerPath) { compilerPath = compilerPath.trim(); if (isCl(compilerPath) || checkExecutableWithoutExtensionExistsSync(compilerPath)) { // If the path ends with cl, or if a file is found at that path, accept it without further validation. compilerName = path.basename(compilerPath); + } else if (cwd && checkExecutableWithoutExtensionExistsSync(path.join(cwd, compilerPath))) { + // If the path is relative and a file is found at that path, accept it without further validation. + compilerPath = path.join(cwd, compilerPath); + compilerName = path.basename(compilerPath); } else if (compilerPath.startsWith("\"") || (os.platform() !== 'win32' && compilerPath.startsWith("'"))) { // If the string starts with a quote, treat it as a command line. // Otherwise, a path with a leading quote would not be valid. - if (useLegacyBehavior) { - compilerArgsFromCommandLineInPath = legacyExtractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - // Try to trim quotes from compiler path. - const tempCompilerPath: string[] | undefined = extractArgs(compilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - compilerPath = tempCompilerPath[0]; - } - compilerName = path.basename(compilerPath); - } - } - } else { - compilerArgsFromCommandLineInPath = extractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - compilerName = path.basename(compilerPath); - } - } + compilerArgsFromCommandLineInPath = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); + if (compilerArgsFromCommandLineInPath.length > 0) { + compilerPath = trimLegacyQuotes(compilerArgsFromCommandLineInPath.shift()); + compilerName = path.basename(compilerPath ?? ''); } } else { - const spaceStart: number = compilerPath.lastIndexOf(" "); - if (spaceStart !== -1) { - // There is no leading quote, but a space suggests it might be a command line. - // Try processing it as a command line, and validate that by checking for the executable. + if (compilerPath.includes(' ')) { + // There is no leading quote, but there is a space so we'll treat it as a command line. const potentialArgs: string[] = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); - let potentialCompilerPath: string | undefined = potentialArgs.shift(); - if (useLegacyBehavior) { - if (potentialCompilerPath) { - const tempCompilerPath: string[] | undefined = extractArgs(potentialCompilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - potentialCompilerPath = tempCompilerPath[0]; - } - } - } - if (potentialCompilerPath) { - if (isCl(potentialCompilerPath) || checkExecutableWithoutExtensionExistsSync(potentialCompilerPath)) { - compilerArgsFromCommandLineInPath = potentialArgs; - compilerPath = potentialCompilerPath; - compilerName = path.basename(compilerPath); - } - } + compilerPath = trimLegacyQuotes(potentialArgs.shift()); + compilerArgsFromCommandLineInPath = potentialArgs; } + compilerName = path.basename(compilerPath ?? ''); } } - let allCompilerArgs: string[] = !compilerArgs ? [] : compilerArgs; - allCompilerArgs = allCompilerArgs.concat(compilerArgsFromCommandLineInPath); + const allCompilerArgs: string[] = (compilerArgs ?? []).concat(compilerArgsFromCommandLineInPath); return { compilerPath, compilerName, compilerArgs, compilerArgsFromCommandLineInPath, allCompilerArgs }; } diff --git a/Extension/test/scenarios/SimpleCppProject/assets/b i n/clang++ b/Extension/test/scenarios/SimpleCppProject/assets/b i n/clang++ new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/assets/b i n/clang++.exe b/Extension/test/scenarios/SimpleCppProject/assets/b i n/clang++.exe new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/assets/bin/cl.exe b/Extension/test/scenarios/SimpleCppProject/assets/bin/cl.exe new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/assets/bin/clang-cl.exe b/Extension/test/scenarios/SimpleCppProject/assets/bin/clang-cl.exe new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/assets/bin/gcc b/Extension/test/scenarios/SimpleCppProject/assets/bin/gcc new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/assets/bin/gcc.exe b/Extension/test/scenarios/SimpleCppProject/assets/bin/gcc.exe new file mode 100644 index 000000000..e69de29bb diff --git a/Extension/test/scenarios/SimpleCppProject/tests/compilerPath.test.ts b/Extension/test/scenarios/SimpleCppProject/tests/compilerPath.test.ts new file mode 100644 index 000000000..c4ee7a633 --- /dev/null +++ b/Extension/test/scenarios/SimpleCppProject/tests/compilerPath.test.ts @@ -0,0 +1,272 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { describe, it } from 'mocha'; +import { deepEqual, equal, ok } from 'node:assert'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { extractCompilerPathAndArgs } from '../../../../src/common'; +import { isWindows } from '../../../../src/constants'; +import { CppProperties } from '../../../../src/LanguageServer/configurations'; + +const assetsFolder = Uri.file(path.normalize(path.join(__dirname.replace(/dist[\/\\]/, ''), '..', 'assets'))); +const assetsFolderFsPath = assetsFolder.fsPath; + +// A simple test counter for the tests that loop over several cases. +// This is to make it easier to see which test failed in the output. +// Start the counter with 1 instead of 0 since we're reporting on test cases, not arrays. +class Counter { + private count: number = 1; + public next(): void { + this.count++; + } + public get str(): string { + return `(test ${this.count})`; + } +} + +if (isWindows) { + describe('extractCompilerPathAndArgs', () => { + // [compilerPath, useLegacyBehavior, additionalArgs, result.compilerName, result.allCompilerArgs] + const nonArgsTests: [string, boolean, string[] | undefined, string, string[]][] = [ + ['cl', false, undefined, 'cl', []], + ['cl.exe', false, undefined, 'cl.exe', []], + [path.join(assetsFolderFsPath, 'bin', 'cl.exe'), false, undefined, 'cl.exe', []], + [path.join(assetsFolderFsPath, 'bin', 'gcc.exe'), false, undefined, 'gcc.exe', []], + [path.join(assetsFolderFsPath, 'b i n', 'clang++.exe'), false, undefined, 'clang++.exe', []], + [path.join(assetsFolderFsPath, 'b i n', 'clang++'), false, undefined, 'clang++', []], + [path.join('bin', 'gcc.exe'), false, undefined, 'gcc.exe', []], + [path.join('bin', 'gcc'), false, undefined, 'gcc', []] + ]; + it('Verify various compilerPath strings without args', () => { + const c = new Counter(); + nonArgsTests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2], assetsFolderFsPath); + ok(result.compilerPath?.endsWith(test[0]), `${c.str} compilerPath should end with ${test[0]}`); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.compilerArgs, test[2], `${c.str} compilerArgs should match`); + deepEqual(result.compilerArgsFromCommandLineInPath, [], `${c.str} compilerArgsFromCommandLineInPath should be empty`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + + // errors and telemetry are set by validateCompilerPath + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + + const argsTests: [string, boolean, string[] | undefined, string, string[]][] = [ + ['cl.exe /c /Fo"test.obj" test.cpp', false, undefined, 'cl.exe', ['/c', '/Fotest.obj', 'test.cpp']], // extra quotes missing, but not needed. + ['cl.exe /c /Fo"test.obj" test.cpp', true, undefined, 'cl.exe', ['/c', '/Fo"test.obj"', 'test.cpp']], + ['cl.exe /c /Fo"test.obj" test.cpp', false, ['/O2'], 'cl.exe', ['/O2', '/c', '/Fotest.obj', 'test.cpp']], + ['cl.exe /c /Fo"test.obj" test.cpp', true, ['/O2'], 'cl.exe', ['/O2', '/c', '/Fo"test.obj"', 'test.cpp']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++.exe')}" -std=c++20`, false, undefined, 'clang++.exe', ['-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++.exe')}" -std=c++20`, true, undefined, 'clang++.exe', ['-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++.exe')}" -std=c++20`, false, ['-O2'], 'clang++.exe', ['-O2', '-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++.exe')}" -std=c++20`, true, ['-O2'], 'clang++.exe', ['-O2', '-std=c++20']], + [`${path.join('bin', 'gcc.exe')} -O2`, false, undefined, 'gcc.exe', ['-O2']], + [`${path.join('bin', 'gcc.exe')} -O2`, true, undefined, 'gcc.exe', ['-O2']] + ]; + it('Verify various compilerPath strings with args', () => { + const c = new Counter(); + argsTests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2]); + const cp = test[0].substring(test[0].at(0) === '"' ? 1 : 0, test[0].indexOf(test[3]) + test[3].length); + ok(result.compilerPath?.endsWith(cp), `${c.str} ${result.compilerPath} !endswith ${cp}`); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.compilerArgs, test[2], `${c.str} compilerArgs should match`); + deepEqual(result.compilerArgsFromCommandLineInPath, test[4].filter(a => !test[2]?.includes(a)), `${c.str} compilerArgsFromCommandLineInPath should match those from the command line`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + + // errors and telemetry are set by validateCompilerPath + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + + const negativeTests: [string, boolean, string[] | undefined, string, string[]][] = [ + [`${path.join(assetsFolderFsPath, 'b i n', 'clang++.exe')} -std=c++20`, false, undefined, 'b', ['i', 'n\\clang++.exe', '-std=c++20']] + ]; + it('Verify various compilerPath strings with args that should fail', () => { + const c = new Counter(); + negativeTests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2]); + ok(result.compilerPath?.endsWith(test[3]), `${c.str} ${result.compilerPath} !endswith ${test[3]}`); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.compilerArgs, test[2], `${c.str} compilerArgs should match`); + deepEqual(result.compilerArgsFromCommandLineInPath, test[4], `${c.str} allCompilerArgs should match`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + + // errors and telemetry are set by validateCompilerPath + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + }); +} else { + describe('extractCompilerPathAndArgs', () => { + // [compilerPath, useLegacyBehavior, additionalArgs, result.compilerName, result.allCompilerArgs] + const tests: [string, boolean, string[] | undefined, string, string[]][] = [ + ['clang', false, undefined, 'clang', []], + [path.join(assetsFolderFsPath, 'bin', 'gcc'), false, undefined, 'gcc', []], + [path.join(assetsFolderFsPath, 'b i n', 'clang++'), false, undefined, 'clang++', []], + [path.join('bin', 'gcc'), false, undefined, 'gcc', []] + ]; + it('Verify various compilerPath strings without args', () => { + const c = new Counter(); + tests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2]); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + + const argsTests: [string, boolean, string[] | undefined, string, string[]][] = [ + ['clang -O2 -Wall', false, undefined, 'clang', ['-O2', '-Wall']], + ['clang -O2 -Wall', true, undefined, 'clang', ['-O2', '-Wall']], + ['clang -O2 -Wall', false, ['-O3'], 'clang', ['-O3', '-O2', '-Wall']], + ['clang -O2 -Wall', true, ['-O3'], 'clang', ['-O3', '-O2', '-Wall']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++')}" -std=c++20`, false, undefined, 'clang++', ['-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++')}" -std=c++20`, true, undefined, 'clang++', ['-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++')}" -std=c++20`, false, ['-O2'], 'clang++', ['-O2', '-std=c++20']], + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++')}" -std=c++20`, true, ['-O2'], 'clang++', ['-O2', '-std=c++20']], + [`${path.join('bin', 'gcc')} -O2`, false, undefined, 'gcc', ['-O2']], + [`${path.join('bin', 'gcc')} -O2`, true, undefined, 'gcc', ['-O2']] + ]; + it('Verify various compilerPath strings with args', () => { + const c = new Counter(); + argsTests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2]); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + + const negativeTests: [string, boolean, string[] | undefined, string, string[]][] = [ + [`${path.join(assetsFolderFsPath, 'b i n', 'clang++')} -std=c++20`, false, undefined, 'b', ['i', 'n/clang++', '-std=c++20']] + ]; + it('Verify various compilerPath strings with args that should fail', () => { + const c = new Counter(); + negativeTests.forEach(test => { + const result = extractCompilerPathAndArgs(test[1], test[0], test[2]); + equal(result.compilerName, test[3], `${c.str} compilerName should match`); + deepEqual(result.allCompilerArgs, test[4], `${c.str} allCompilerArgs should match`); + + // errors and telemetry are set by validateCompilerPath + equal(result.error, undefined, `${c.str} error should be undefined`); + equal(result.telemetry, undefined, `${c.str} telemetry should be undefined`); + c.next(); + }); + }); + }); +} + +describe('validateCompilerPath', () => { + // [compilerPath, cwd, result.compilerName, result.allCompilerArgs, result.error, result.telemetry] + const tests: [string, Uri, string, string[]][] = [ + ['cl.exe', assetsFolder, 'cl.exe', []], + ['cl', assetsFolder, 'cl', []], + ['clang', assetsFolder, 'clang', []], + [path.join(assetsFolderFsPath, 'bin', 'cl'), assetsFolder, 'cl', []], + [path.join(assetsFolderFsPath, 'bin', 'clang-cl'), assetsFolder, 'clang-cl', []], + [path.join(assetsFolderFsPath, 'bin', 'gcc'), assetsFolder, 'gcc', []], + [path.join(assetsFolderFsPath, 'b i n', 'clang++'), assetsFolder, 'clang++', []], + [path.join('bin', 'gcc'), assetsFolder, 'gcc', []], + [path.join('bin', 'clang-cl'), assetsFolder, 'clang-cl', []], + ['', assetsFolder, '', []], + [' cl.exe ', assetsFolder, 'cl.exe', []] + ]; + it('Verify various compilerPath strings without args', () => { + const c = new Counter(); + tests.forEach(test => { + // Skip the clang-cl test on non-Windows. That test is for checking the addition of .exe to the compiler name on Windows only. + if (isWindows || !test[0].includes('clang-cl')) { + const result = CppProperties.validateCompilerPath(test[0], test[1]); + equal(result.compilerName, test[2], `${c.str} compilerName should match`); + deepEqual(result.allCompilerArgs, test[3], `${c.str} allCompilerArgs should match`); + equal(result.error, undefined, `${c.str} error should be undefined`); + deepEqual(result.telemetry, test[0] === '' ? undefined : {}, `${c.str} telemetry should be empty`); + } + c.next(); + }); + }); + + const argsTests: [string, Uri, string, string[]][] = [ + ['cl.exe /std:c++20 /O2', assetsFolder, 'cl.exe', ['/std:c++20', '/O2']], // issue with /Fo"test.obj" argument + [`"${path.join(assetsFolderFsPath, 'b i n', 'clang++')}" -std=c++20 -O2`, assetsFolder, 'clang++', ['-std=c++20', '-O2']], + [`${path.join('bin', 'gcc')} -std=c++20 -Wall`, assetsFolder, 'gcc', ['-std=c++20', '-Wall']], + ['clang -O2 -Wall', assetsFolder, 'clang', ['-O2', '-Wall']] + ]; + it('Verify various compilerPath strings with args', () => { + const c = new Counter(); + argsTests.forEach(test => { + const result = CppProperties.validateCompilerPath(test[0], test[1]); + equal(result.compilerName, test[2], `${c.str} compilerName should match`); + deepEqual(result.allCompilerArgs, test[3], `${c.str} allCompilerArgs should match`); + equal(result.error, undefined, `${c.str} error should be undefined`); + deepEqual(result.telemetry, {}, `${c.str} telemetry should be empty`); + c.next(); + }); + }); + + it('Verify errors with invalid relative compiler path', async () => { + const result = CppProperties.validateCompilerPath(path.join('assets', 'bin', 'gcc'), assetsFolder); + equal(result.compilerName, 'gcc', 'compilerName should be found'); + equal(result.allCompilerArgs.length, 0, 'Should not have any args'); + ok(result.error?.includes('Cannot find'), 'Should have an error for relative paths'); + equal(result.telemetry?.PathNonExistent, 1, 'Should have telemetry for relative paths'); + equal(result.telemetry?.PathNotAFile, undefined, 'Should not have telemetry for invalid paths'); + equal(result.telemetry?.CompilerPathMissingQuotes, undefined, 'Should not have telemetry for missing quotes'); + }); + + it('Verify errors with invalid absolute compiler path', async () => { + const result = CppProperties.validateCompilerPath(path.join(assetsFolderFsPath, 'assets', 'bin', 'gcc'), assetsFolder); + equal(result.compilerName, 'gcc', 'compilerName should be found'); + equal(result.allCompilerArgs.length, 0, 'Should not have any args'); + ok(result.error?.includes('Cannot find'), 'Should have an error for absolute paths'); + equal(result.telemetry?.PathNonExistent, 1, 'Should have telemetry for absolute paths'); + equal(result.telemetry?.PathNotAFile, undefined, 'Should not have telemetry for invalid paths'); + equal(result.telemetry?.CompilerPathMissingQuotes, undefined, 'Should not have telemetry for missing quotes'); + }); + + it('Verify errors with non-file compilerPath', async () => { + const result = CppProperties.validateCompilerPath('bin', assetsFolder); + equal(result.compilerName, 'bin', 'compilerName should be found'); + equal(result.allCompilerArgs.length, 0, 'Should not have any args'); + ok(result.error?.includes('Path is not a file'), 'Should have an error for non-file paths'); + equal(result.telemetry?.PathNonExistent, undefined, 'Should not have telemetry for relative paths'); + equal(result.telemetry?.PathNotAFile, 1, 'Should have telemetry for invalid paths'); + equal(result.telemetry?.CompilerPathMissingQuotes, undefined, 'Should not have telemetry for missing quotes'); + }); + + it('Verify errors with unknown compiler not in Path', async () => { + const result = CppProperties.validateCompilerPath('icc', assetsFolder); + equal(result.compilerName, 'icc', 'compilerName should be found'); + equal(result.allCompilerArgs.length, 0, 'Should not have any args'); + equal(result.telemetry?.PathNonExistent, 1, 'Should have telemetry for relative paths'); + equal(result.telemetry?.PathNotAFile, undefined, 'Should not have telemetry for invalid paths'); + equal(result.telemetry?.CompilerPathMissingQuotes, undefined, 'Should not have telemetry for missing quotes'); + }); + + it('Verify errors with unknown compiler not in Path with args', async () => { + const result = CppProperties.validateCompilerPath('icc -O2', assetsFolder); + equal(result.compilerName, 'icc', 'compilerName should be found'); + deepEqual(result.allCompilerArgs, ['-O2'], 'args should match'); + ok(result.error?.includes('Cannot find'), 'Should have an error for unknown compiler'); + ok(result.error?.includes('surround the compiler path with double quotes'), 'Should have an error for missing double quotes'); + equal(result.telemetry?.PathNonExistent, 1, 'Should have telemetry for relative paths'); + equal(result.telemetry?.PathNotAFile, undefined, 'Should not have telemetry for invalid paths'); + equal(result.telemetry?.CompilerPathMissingQuotes, 1, 'Should have telemetry for missing quotes'); + }); + +});