Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 7, 2025

Summary

The linter cache filename used a hash of all input filenames, causing cache churn when files were added/removed. This stabilizes the cache filename by hashing the tsconfig path instead, while moving the files hash into the cache data for validation.

Fixes #5436

Details

Cache filename generation (LinterBase.ts):

  • Hash suffix now derived from project-relative tsconfig path extracted from program.getCompilerOptions().configFilePath
  • Files hash moved to ILinterCacheData.filesHash field for cache validation
  • File paths are sorted before hashing to ensure stable hash values
  • Uses base64url encoding for hashes (filesHash without truncation, hashSuffix truncated to 8 chars for filename)
  • Cache invalidated if either cacheVersion or filesHash changes

Simplified implementation (LintPlugin.ts):

  • No manual tsconfig path tracking needed - extracted directly from TypeScript program
  • Cleaner code without additional tuple elements or parameter forwarding

Before:

// Cache filename: _eslint-xj9kL2mP.json (hash of all file paths)
// Changes when files added/removed

After:

// Cache filename: _eslint-a7B3cD4e.json (hash of "tsconfig.json" path)
// Stable across file changes, invalidated via filesHash in cache data

How it was tested

  • ✅ All existing unit tests pass (8 tests)
  • ✅ Build succeeds without errors
  • ✅ CodeQL security scan shows no vulnerabilities
  • Manual verification that cache filename is now stable based on tsconfig path

Impacted documentation

None - this is an internal implementation change that does not affect the public API or user-facing behavior.

Original prompt

This section details on the original issue you should resolve

<issue_title>[heft-lint-plugin] Stabilize the hash suffix in the linter cache file</issue_title>
<issue_description>

const relativePaths: Map<string, string> = new Map();
const fileHash: Hash = createHash('md5');
for (const file of options.typeScriptFilenames) {
// Need to use relative paths to ensure portability.
const relative: string = Path.convertToSlashes(path.relative(commonDirectory, file));
relativePaths.set(file, relative);
fileHash.update(relative);
}
const hashSuffix: string = fileHash.digest('base64').replace(/\+/g, '-').replace(/\//g, '_').slice(0, 8);

Update the hash calculation for the linter cache filename to only be a hash of the project-relative path of the tsconfig file that corresponds to the program instance, rather than a hash of the list of file names.

The hash of the list of filenames may be placed inside of the cache file instead as a check to verify that it was run with the same files.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Stabilize hash suffix in linter cache file [heft-lint-plugin] Stabilize linter cache filename using tsconfig path hash Nov 7, 2025
Copilot AI requested a review from dmichon-msft November 7, 2025 23:21
Copilot finished work on behalf of dmichon-msft November 7, 2025 23:21
Copy link
Contributor

@dmichon-msft dmichon-msft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get the config file path from the program's getCompilerOptions().configFilePath (may require null checking). This will greatly simplify the code change.

accessor.onChangedFilesHook.tap(PLUGIN_NAME, (changedFilesHookOptions: IChangedFilesHookOptions) => {
// When using the TypeScript plugin, we need to determine the tsconfig path
// The default tsconfig path is used when not explicitly specified
const tsconfigPath: string = path.resolve(heftConfiguration.buildFolderPath, 'tsconfig.json');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it is not documented on the public API typings, changedFilesHookOptions.program.getCompilerOptions().configFilePath is the resolved path to the tsconfig file used by the program, and simply needs to be converted to a relative path and be run through Path.convertToSlashes.

heft-typescript-plugin already uses this code path, so the custom extended type should already be defined in the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit ba0baea. Now extracting configFilePath directly from program.getCompilerOptions().configFilePath.

taskSession
);
typescriptChangedFiles.push([tsProgram, new Set(tsProgram.getSourceFiles())]);
typescriptChangedFiles.push([tsProgram, new Set(tsProgram.getSourceFiles()), tsconfigPath]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the path is extractable from the program, you don't need to forward it separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit ba0baea. Removed the separate forwarding of tsconfigFilePath.

Comment on lines 106 to 109
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.slice(0, 8);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.slice(0, 8);
.digest('base64url');

No need to truncate since it is not in a length-limited location.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit ba0baea. Changed to base64url encoding without truncation for filesHash.

Comment on lines 118 to 120
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.digest('base64url')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit ba0baea. Changed to base64url encoding for the cache filename hash suffix.

const fileHash: Hash = createHash('md5');
// Calculate the hash of the list of filenames for verification purposes
const filesHash: Hash = createHash('md5');
for (const file of options.typeScriptFilenames) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy this array and then sort it to ensure a stable hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit ba0baea. Now sorting the file paths array before hashing to ensure stable hash values.

@dmichon-msft
Copy link
Contributor

@copilot address feedback

…se64url encoding, and sort files

Co-authored-by: dmichon-msft <[email protected]>
Copy link
Contributor Author

Copilot AI commented Nov 8, 2025

Get the config file path from the program's getCompilerOptions().configFilePath (may require null checking). This will greatly simplify the code change.

Done in commit ba0baea. The implementation now uses program.getCompilerOptions().configFilePath as suggested, which greatly simplified the code.

Copilot finished work on behalf of dmichon-msft November 8, 2025 01:35
Copilot AI requested a review from dmichon-msft November 8, 2025 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

[heft-lint-plugin] Stabilize the hash suffix in the linter cache file

2 participants