diff --git a/Extension/src/LanguageServer/clangTidyDocumentation.ts b/Extension/src/LanguageServer/clangTidyDocumentation.ts new file mode 100644 index 000000000..1c9a30b1e --- /dev/null +++ b/Extension/src/LanguageServer/clangTidyDocumentation.ts @@ -0,0 +1,26 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +/** + * The clang-tidy documentation is published per major LLVM release, so a check's page lives + * under the .1.0 release of the major the binary in use comes from: 23.1.0 and 22.1.0 have + * pages, 23.1.1 and 22.1.3 do not. Checks added in a newer major have no page in an older one, + * so the URL follows the binary in use rather than the release bundled with the extension. + * When the version in use could not be determined, fall back to the bundled release. + */ +export const fallbackClangTidyDocVersion: string = '23.1.0'; + +const versionPattern: RegExp = /^([0-9]+)\.[0-9]+\.[0-9]+$/; + +/** The release a clang-tidy version's documentation was published under. */ +function documentationRelease(clangTidyVersion: string): string | undefined { + const match: RegExpMatchArray | null = clangTidyVersion.match(versionPattern); + return match ? `${match[1]}.1.0` : undefined; +} + +export function getClangTidyDocumentationUrl(docPage: string, clangTidyVersion?: string): string { + const release: string | undefined = clangTidyVersion ? documentationRelease(clangTidyVersion) : undefined; + return `https://releases.llvm.org/${release ?? fallbackClangTidyDocVersion}/tools/clang/tools/extra/docs/clang-tidy/${docPage}`; +} diff --git a/Extension/src/LanguageServer/codeAnalysis.ts b/Extension/src/LanguageServer/codeAnalysis.ts index 61cc8cf06..e6f7a2b23 100644 --- a/Extension/src/LanguageServer/codeAnalysis.ts +++ b/Extension/src/LanguageServer/codeAnalysis.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import { NotificationType, Range } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; +import { getClangTidyDocumentationUrl } from './clangTidyDocumentation'; import { Location, WorkspaceEdit } from './commonTypes'; import { CppSourceStr } from './extension'; import { LanguageClient } from './languageClient'; @@ -379,8 +380,7 @@ export function publishCodeAnalysisDiagnostics(params: PublishCodeAnalysisDiagno const checksPage: string = dashIndex > 0 ? primaryCode.substring(dashIndex + 1) : primaryCode; docPage = `checks${checksGroup}/${checksPage}.html`; } - // TODO: This should be checking the clang-tidy version used to better support usage of older versions. - const primaryDocUri: vscode.Uri = vscode.Uri.parse(`https://releases.llvm.org/23.1.0/tools/clang/tools/extra/docs/clang-tidy/${docPage}`); + const primaryDocUri: vscode.Uri = vscode.Uri.parse(getClangTidyDocumentationUrl(docPage, new CppSettings(realUri).clangTidyVersion)); diagnostic.code = { value: identifier.code, target: primaryDocUri }; if (new CppSettings().clangTidyCodeActionShowDocumentation) { diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 499431d09..c5916dbdc 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -12,7 +12,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as which from 'which'; import * as util from '../common'; -import { getCachedClangFormatPath, getCachedClangTidyPath, getExtensionFilePath, getRawSetting, isArray, isArrayOfString, isBoolean, isNumber, isObject, isString, isValidMapping, setCachedClangFormatPath, setCachedClangTidyPath } from '../common'; +import { getCachedClangFormatPath, getCachedClangTidyPath, getCachedClangTidyVersion, getExtensionFilePath, getRawSetting, isArray, isArrayOfString, isBoolean, isNumber, isObject, isString, isValidMapping, setCachedClangFormatPath, setCachedClangTidyPath, setCachedClangTidyVersion } from '../common'; import { isWindows } from '../constants'; import * as telemetry from '../telemetry'; import { cachedEditorConfigLookups, DefaultClient, hasTrustedCompilerPaths } from './client'; @@ -279,6 +279,14 @@ export class CppSettings extends Settings { return this.getClangPath(false); } + public get clangTidyVersion(): string | undefined { + // The cached version comes from an auto-discovered binary, so it says nothing about the + // clang-tidy a resource carrying its own path is running. Reporting it would point that + // resource's documentation links at the wrong release, so leave the version unknown and + // let the caller fall back. + return changeBlankStringToUndefined(this.getAsStringOrUndefined("codeAnalysis.clangTidy.path")) ? undefined : getCachedClangTidyVersion(); + } + public get clangFormatPath(): string | undefined { return this.getClangPath(true); } @@ -303,6 +311,7 @@ export class CppSettings extends Settings { } else { // Attempt to invoke both our own version of clang-* to see if we can successfully execute it, and to get its version. let bundledVersion: string; + let clangVersion: string | undefined; try { const bundledPath: string = getExtensionFilePath(`./LLVM/bin/${clangName}`); const output: string = execFileSync(bundledPath, ['--version']).toString(); @@ -322,10 +331,18 @@ export class CppSettings extends Settings { if (semver.ltr(userVersion, bundledVersion)) { path = ""; setCachedClangPath(path); + clangVersion = bundledVersion; + } else { + clangVersion = userVersion; } } catch { path = ""; setCachedClangPath(path); + clangVersion = bundledVersion; + } + + if (!isFormat && clangVersion) { + setCachedClangTidyVersion(clangVersion); } } } diff --git a/Extension/src/common.ts b/Extension/src/common.ts index 700d8ab44..1022d2b8c 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -60,6 +60,15 @@ export function setCachedClangTidyPath(path: string): void { cachedClangTidyPath = path; } +let cachedClangTidyVersion: string | undefined; +export function getCachedClangTidyVersion(): string | undefined { + return cachedClangTidyVersion; +} + +export function setCachedClangTidyVersion(version: string): void { + cachedClangTidyVersion = version; +} + // Use this package.json to read values export const packageJson: any = vscode.extensions.getExtension("ms-vscode.cpptools")?.packageJSON; diff --git a/Extension/test/unit/clangTidyDocumentation.test.ts b/Extension/test/unit/clangTidyDocumentation.test.ts new file mode 100644 index 000000000..a6fccc710 --- /dev/null +++ b/Extension/test/unit/clangTidyDocumentation.test.ts @@ -0,0 +1,33 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { describe, it } from 'mocha'; +import { strictEqual } from 'node:assert'; +import { fallbackClangTidyDocVersion, getClangTidyDocumentationUrl } from '../../src/LanguageServer/clangTidyDocumentation'; + +const bundledDocUrl: string = `https://releases.llvm.org/${fallbackClangTidyDocVersion}/tools/clang/tools/extra/docs/clang-tidy`; + +describe('clang-tidy documentation URLs', () => { + it('points at the clang-tidy version in use', () => { + strictEqual( + getClangTidyDocumentationUrl('checks/readability/use-std-min-max.html', '19.1.0'), + 'https://releases.llvm.org/19.1.0/tools/clang/tools/extra/docs/clang-tidy/checks/readability/use-std-min-max.html'); + }); + + it('uses the .1.0 release of that major, since patch releases have no page', () => { + strictEqual( + getClangTidyDocumentationUrl('checks/readability/use-std-min-max.html', '23.1.1'), + 'https://releases.llvm.org/23.1.0/tools/clang/tools/extra/docs/clang-tidy/checks/readability/use-std-min-max.html'); + strictEqual( + getClangTidyDocumentationUrl('checks/misc/include-cleaner.html', '22.1.3'), + 'https://releases.llvm.org/22.1.0/tools/clang/tools/extra/docs/clang-tidy/checks/misc/include-cleaner.html'); + }); + + it('falls back to the bundled version when the version in use is unknown', () => { + strictEqual(getClangTidyDocumentationUrl('checks/misc/include-cleaner.html'), `${bundledDocUrl}/checks/misc/include-cleaner.html`); + strictEqual(getClangTidyDocumentationUrl('checks/misc/include-cleaner.html', ''), `${bundledDocUrl}/checks/misc/include-cleaner.html`); + strictEqual(getClangTidyDocumentationUrl('checks/misc/include-cleaner.html', '23.1'), `${bundledDocUrl}/checks/misc/include-cleaner.html`); + }); +});