Skip to content

Commit fb6faf2

Browse files
[heft-lint-plugin] Stabilize linter cache filename using tsconfig path hash (#5437)
* Initial plan * Stabilize hash suffix in linter cache file based on tsconfig path Co-authored-by: dmichon-msft <[email protected]> * Address feedback: use program.getCompilerOptions().configFilePath, base64url encoding, and sort files Co-authored-by: dmichon-msft <[email protected]> * Add rush change file for heft-lint-plugin patch Co-authored-by: dmichon-msft <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: dmichon-msft <[email protected]>
1 parent 69f3b21 commit fb6faf2

File tree

2 files changed

+53
-5
lines changed

2 files changed

+53
-5
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Stabilize the hash suffix in the linter cache file by using tsconfig path hash instead of file list hash",
5+
"type": "patch",
6+
"packageName": "@rushstack/heft-lint-plugin"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-lint-plugin",
10+
"email": "[email protected]"
11+
}

heft-plugins/heft-lint-plugin/src/LinterBase.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as path from 'node:path';
55
import { performance } from 'node:perf_hooks';
66
import { createHash, type Hash } from 'node:crypto';
77

8+
import type * as TTypescript from 'typescript';
9+
810
import { FileSystem, JsonFile, Path } from '@rushstack/node-core-library';
911
import type { ITerminal } from '@rushstack/terminal';
1012
import type { IScopedLogger } from '@rushstack/heft';
@@ -51,6 +53,12 @@ interface ILinterCacheData {
5153
* each array item is the file's path and the second element is the file's hash.
5254
*/
5355
fileVersions: [string, string][];
56+
57+
/**
58+
* A hash of the list of filenames that were linted. This is used to verify that
59+
* the cache was run with the same files.
60+
*/
61+
filesHash?: string;
5462
}
5563

5664
export abstract class LinterBase<TLintResult> {
@@ -85,14 +93,40 @@ export abstract class LinterBase<TLintResult> {
8593

8694
const relativePaths: Map<string, string> = new Map();
8795

88-
const fileHash: Hash = createHash('md5');
96+
// Collect and sort file paths for stable hashing
97+
const relativePathsArray: string[] = [];
8998
for (const file of options.typeScriptFilenames) {
9099
// Need to use relative paths to ensure portability.
91100
const relative: string = Path.convertToSlashes(path.relative(commonDirectory, file));
92101
relativePaths.set(file, relative);
93-
fileHash.update(relative);
102+
relativePathsArray.push(relative);
103+
}
104+
relativePathsArray.sort();
105+
106+
// Calculate the hash of the list of filenames for verification purposes
107+
const filesHash: Hash = createHash('md5');
108+
for (const relative of relativePathsArray) {
109+
filesHash.update(relative);
110+
}
111+
const filesHashString: string = filesHash.digest('base64url');
112+
113+
// Calculate the hash suffix based on the project-relative path of the tsconfig file
114+
// Extract the config file path from the program's compiler options
115+
const compilerOptions: TTypescript.CompilerOptions = options.tsProgram.getCompilerOptions();
116+
const tsconfigFilePath: string | undefined = compilerOptions.configFilePath as string | undefined;
117+
118+
let hashSuffix: string;
119+
if (tsconfigFilePath) {
120+
const relativeTsconfigPath: string = Path.convertToSlashes(
121+
path.relative(this._buildFolderPath, tsconfigFilePath)
122+
);
123+
const tsconfigHash: Hash = createHash('md5');
124+
tsconfigHash.update(relativeTsconfigPath);
125+
hashSuffix = tsconfigHash.digest('base64url').slice(0, 8);
126+
} else {
127+
// Fallback to a default hash if configFilePath is not available
128+
hashSuffix = 'default';
94129
}
95-
const hashSuffix: string = fileHash.digest('base64').replace(/\+/g, '-').replace(/\//g, '_').slice(0, 8);
96130

97131
const linterCacheVersion: string = await this.getCacheVersionAsync();
98132
const linterCacheFilePath: string = path.resolve(
@@ -121,7 +155,9 @@ export abstract class LinterBase<TLintResult> {
121155
}
122156

123157
const cachedNoFailureFileVersions: Map<string, string> = new Map<string, string>(
124-
linterCacheData?.cacheVersion === linterCacheVersion ? linterCacheData.fileVersions : []
158+
linterCacheData?.cacheVersion === linterCacheVersion && linterCacheData?.filesHash === filesHashString
159+
? linterCacheData.fileVersions
160+
: []
125161
);
126162

127163
const newNoFailureFileVersions: Map<string, string> = new Map<string, string>();
@@ -173,7 +209,8 @@ export abstract class LinterBase<TLintResult> {
173209

174210
const updatedTslintCacheData: ILinterCacheData = {
175211
cacheVersion: linterCacheVersion,
176-
fileVersions: Array.from(newNoFailureFileVersions)
212+
fileVersions: Array.from(newNoFailureFileVersions),
213+
filesHash: filesHashString
177214
};
178215
await JsonFile.saveAsync(updatedTslintCacheData, linterCacheFilePath, { ensureFolderExists: true });
179216

0 commit comments

Comments
 (0)