Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions heft-plugins/heft-lint-plugin/src/LintPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface ILintOptions {
taskSession: IHeftTaskSession;
heftConfiguration: HeftConfiguration;
tsProgram: IExtendedProgram;
tsconfigFilePath: string;
fix?: boolean;
sarifLogPath?: string;
changedFiles?: ReadonlySet<IExtendedSourceFile>;
Expand Down Expand Up @@ -104,7 +105,8 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
let inTypescriptPhase: boolean = false;

// Use the changed files hook to collect the files and programs from TypeScript
let typescriptChangedFiles: [IExtendedProgram, ReadonlySet<IExtendedSourceFile>][] = [];
// Also track the tsconfig path for cache file naming
let typescriptChangedFiles: [IExtendedProgram, ReadonlySet<IExtendedSourceFile>, string][] = [];
taskSession.requestAccessToPluginByName(
TYPESCRIPT_PLUGIN_PACKAGE_NAME,
TYPESCRIPT_PLUGIN_NAME,
Expand All @@ -114,9 +116,13 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {

// Hook into the changed files hook to collect the changed files and their programs
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');
typescriptChangedFiles.push([
changedFilesHookOptions.program as IExtendedProgram,
changedFilesHookOptions.changedFiles as ReadonlySet<IExtendedSourceFile>
changedFilesHookOptions.changedFiles as ReadonlySet<IExtendedSourceFile>,
tsconfigPath
]);
});
}
Expand All @@ -126,20 +132,22 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
// If we are not in the typescript phase, we need to create a typescript program
// from the tsconfig file
if (!inTypescriptPhase) {
const tsconfigPath: string = path.resolve(heftConfiguration.buildFolderPath, 'tsconfig.json');
const tsProgram: IExtendedProgram = await this._createTypescriptProgramAsync(
heftConfiguration,
taskSession
);
typescriptChangedFiles.push([tsProgram, new Set(tsProgram.getSourceFiles())]);
typescriptChangedFiles.push([tsProgram, new Set(tsProgram.getSourceFiles()), tsconfigPath]);
}

// Run the linters to completion. Linters emit errors and warnings to the logger.
for (const [tsProgram, changedFiles] of typescriptChangedFiles) {
for (const [tsProgram, changedFiles, tsconfigFilePath] of typescriptChangedFiles) {
try {
await this._lintAsync({
taskSession,
heftConfiguration,
tsProgram,
tsconfigFilePath,
changedFiles,
fix,
sarifLogPath
Expand Down Expand Up @@ -222,7 +230,8 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
}

private async _lintAsync(options: ILintOptions): Promise<void> {
const { taskSession, heftConfiguration, tsProgram, changedFiles, fix, sarifLogPath } = options;
const { taskSession, heftConfiguration, tsProgram, tsconfigFilePath, changedFiles, fix, sarifLogPath } =
options;

// Ensure that we have initialized. This promise is cached, so calling init
// multiple times will only init once.
Expand All @@ -232,6 +241,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
if (this._eslintConfigFilePath && this._eslintToolPath) {
const eslintLinter: Eslint = await Eslint.initializeAsync({
tsProgram,
tsconfigFilePath,
fix,
sarifLogPath,
scopedLogger: taskSession.logger,
Expand All @@ -246,6 +256,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
if (this._tslintConfigFilePath && this._tslintToolPath) {
const tslintLinter: Tslint = await Tslint.initializeAsync({
tsProgram,
tsconfigFilePath,
fix,
scopedLogger: taskSession.logger,
linterToolPath: this._tslintToolPath,
Expand Down
39 changes: 34 additions & 5 deletions heft-plugins/heft-lint-plugin/src/LinterBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ILinterBaseOptions {
linterToolPath: string;
linterConfigFilePath: string;
tsProgram: IExtendedProgram;
tsconfigFilePath: string;
fix?: boolean;
sarifLogPath?: string;
}
Expand Down Expand Up @@ -51,6 +52,12 @@ interface ILinterCacheData {
* each array item is the file's path and the second element is the file's hash.
*/
fileVersions: [string, string][];

/**
* A hash of the list of filenames that were linted. This is used to verify that
* the cache was run with the same files.
*/
filesHash?: string;
}

export abstract class LinterBase<TLintResult> {
Expand All @@ -59,6 +66,7 @@ export abstract class LinterBase<TLintResult> {
protected readonly _buildFolderPath: string;
protected readonly _buildMetadataFolderPath: string;
protected readonly _linterConfigFilePath: string;
protected readonly _tsconfigFilePath: string;
protected readonly _fix: boolean;

protected _fixesPossible: boolean = false;
Expand All @@ -71,6 +79,7 @@ export abstract class LinterBase<TLintResult> {
this._buildFolderPath = options.buildFolderPath;
this._buildMetadataFolderPath = options.buildMetadataFolderPath;
this._linterConfigFilePath = options.linterConfigFilePath;
this._tsconfigFilePath = options.tsconfigFilePath;
this._linterName = linterName;
this._fix = options.fix || false;
}
Expand All @@ -85,14 +94,31 @@ export abstract class LinterBase<TLintResult> {

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

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) {
// Need to use relative paths to ensure portability.
const relative: string = Path.convertToSlashes(path.relative(commonDirectory, file));
relativePaths.set(file, relative);
fileHash.update(relative);
filesHash.update(relative);
}
const hashSuffix: string = fileHash.digest('base64').replace(/\+/g, '-').replace(/\//g, '_').slice(0, 8);
const filesHashString: string = filesHash
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.slice(0, 8);

// Calculate the hash suffix based on the project-relative path of the tsconfig file
const relativeTsconfigPath: string = Path.convertToSlashes(
path.relative(this._buildFolderPath, this._tsconfigFilePath)
);
const tsconfigHash: Hash = createHash('md5');
tsconfigHash.update(relativeTsconfigPath);
const hashSuffix: string = tsconfigHash
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.slice(0, 8);

const linterCacheVersion: string = await this.getCacheVersionAsync();
const linterCacheFilePath: string = path.resolve(
Expand Down Expand Up @@ -121,7 +147,9 @@ export abstract class LinterBase<TLintResult> {
}

const cachedNoFailureFileVersions: Map<string, string> = new Map<string, string>(
linterCacheData?.cacheVersion === linterCacheVersion ? linterCacheData.fileVersions : []
linterCacheData?.cacheVersion === linterCacheVersion && linterCacheData?.filesHash === filesHashString
? linterCacheData.fileVersions
: []
);

const newNoFailureFileVersions: Map<string, string> = new Map<string, string>();
Expand Down Expand Up @@ -172,7 +200,8 @@ export abstract class LinterBase<TLintResult> {

const updatedTslintCacheData: ILinterCacheData = {
cacheVersion: linterCacheVersion,
fileVersions: Array.from(newNoFailureFileVersions)
fileVersions: Array.from(newNoFailureFileVersions),
filesHash: filesHashString
};
await JsonFile.saveAsync(updatedTslintCacheData, linterCacheFilePath, { ensureFolderExists: true });

Expand Down
Loading