Skip to content

[heft-lint] Adds support for using lint plugin without typescript phase #5239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-lint-plugin",
"comment": "Adds support for using heft-lint-plugin standalone without a typescript phase",
"type": "patch"
}
],
"packageName": "@rushstack/heft-lint-plugin"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "065bbcb7e1368f568ac47b0f1ec2aa5ae4cbce31",
"pnpmShrinkwrapHash": "c7ba4d11d03d9e1b14ba33e023a043d385fa3fd8",
"preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68",
"packageJsonInjectedDependenciesHash": "cd56ac34ee98801d8760b47b63a5686adc8ade1d"
"packageJsonInjectedDependenciesHash": "de32ff5fe062252f7310c4fb86383790757d735c"
}
4 changes: 2 additions & 2 deletions heft-plugins/heft-lint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@types/semver": "7.5.0",
"decoupled-local-node-rig": "workspace:*",
"eslint": "~8.57.0",
"tslint": "~5.20.1",
"typescript": "~5.8.2"
"typescript": "~5.8.2",
"tslint": "~5.20.1"
}
}
102 changes: 87 additions & 15 deletions heft-plugins/heft-lint-plugin/src/LintPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type {
ITypeScriptPluginAccessor
} from '@rushstack/heft-typescript-plugin';

import type * as TTypescript from 'typescript';

import type { LinterBase } from './LinterBase';
import { Eslint } from './Eslint';
import { Tslint } from './Tslint';
Expand Down Expand Up @@ -52,29 +54,43 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
private _tslintToolPath: string | undefined;
private _tslintConfigFilePath: string | undefined;

private _checkFix(taskSession: IHeftTaskSession, pluginOptions?: ILintPluginOptions): boolean {
let fix: boolean =
pluginOptions?.alwaysFix || taskSession.parameters.getFlagParameter(FIX_PARAMETER_NAME).value;
if (fix && taskSession.parameters.production) {
// Write this as a standard output message since we don't want to throw errors when running in
// production mode and "alwaysFix" is specified in the plugin options
taskSession.logger.terminal.writeLine(
'Fix mode has been disabled since Heft is running in production mode'
);
fix = false;
}
return fix;
}

private _getSarifLogPath(
heftConfiguration: HeftConfiguration,
pluginOptions?: ILintPluginOptions
): string | undefined {
const relativeSarifLogPath: string | undefined = pluginOptions?.sarifLogPath;
const sarifLogPath: string | undefined =
relativeSarifLogPath && path.resolve(heftConfiguration.buildFolderPath, relativeSarifLogPath);
return sarifLogPath;
}

public apply(
taskSession: IHeftTaskSession,
heftConfiguration: HeftConfiguration,
pluginOptions?: ILintPluginOptions
): void {
// To support standalone linting, track if we have hooked to the typescript plugin
let inTypescriptPhase: boolean = false;

// Disable linting in watch mode. Some lint rules require the context of multiple files, which
// may not be available in watch mode.
if (!taskSession.parameters.watch) {
let fix: boolean =
pluginOptions?.alwaysFix || taskSession.parameters.getFlagParameter(FIX_PARAMETER_NAME).value;
if (fix && taskSession.parameters.production) {
// Write this as a standard output message since we don't want to throw errors when running in
// production mode and "alwaysFix" is specified in the plugin options
taskSession.logger.terminal.writeLine(
'Fix mode has been disabled since Heft is running in production mode'
);
fix = false;
}

const relativeSarifLogPath: string | undefined = pluginOptions?.sarifLogPath;
const sarifLogPath: string | undefined =
relativeSarifLogPath && path.resolve(heftConfiguration.buildFolderPath, relativeSarifLogPath);

const fix: boolean = this._checkFix(taskSession, pluginOptions);
const sarifLogPath: string | undefined = this._getSarifLogPath(heftConfiguration, pluginOptions);
// Use the changed files hook to kick off linting asynchronously
taskSession.requestAccessToPluginByName(
'@rushstack/heft-typescript-plugin',
Expand All @@ -99,6 +115,8 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
this._lintingPromises.push(lintingPromise);
}
);
// Set the flag to indicate that we are in the typescript phase
inTypescriptPhase = true;
}
);
}
Expand All @@ -116,11 +134,65 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
// Warn since don't run the linters when in watch mode.
taskSession.logger.terminal.writeWarningLine("Linting isn't currently supported in watch mode");
} else {
if (!inTypescriptPhase) {
const fix: boolean = this._checkFix(taskSession, pluginOptions);
const sarifLogPath: string | undefined = this._getSarifLogPath(heftConfiguration, pluginOptions);
// If we are not in the typescript phase, we need to create a typescript program
// from the tsconfig file
const tsProgram: IExtendedProgram = await this._createTypescriptProgramAsync(
heftConfiguration,
taskSession
);
const rootFiles: readonly string[] = tsProgram.getRootFileNames();
const changedFiles: Set<IExtendedSourceFile> = new Set<IExtendedSourceFile>();
rootFiles.forEach((rootFilePath: string) => {
const sourceFile: TTypescript.SourceFile | undefined = tsProgram.getSourceFile(rootFilePath);
changedFiles.add(sourceFile as IExtendedSourceFile);
});

const lintingPromise: Promise<void> = this._lintAsync({
taskSession,
heftConfiguration,
fix,
sarifLogPath,
tsProgram,
changedFiles
});
lintingPromise.catch(() => {
// Suppress unhandled promise rejection error
});
// Hold on to the original promise, which will throw in the run hook if it unexpectedly fails
this._lintingPromises.push(lintingPromise);
}
await Promise.all(this._lintingPromises);
}
});
}

private async _createTypescriptProgramAsync(
heftConfiguration: HeftConfiguration,
taskSession: IHeftTaskSession
): Promise<IExtendedProgram> {
const typescriptPath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync(
'typescript',
taskSession.logger.terminal
);
const ts: typeof TTypescript = await import(typescriptPath);
// Create a typescript program from the tsconfig file
const tsconfigPath: string = path.resolve(heftConfiguration.buildFolderPath, 'tsconfig.json');
const parsed: TTypescript.ParsedCommandLine = ts.parseJsonConfigFileContent(
ts.readConfigFile(tsconfigPath, ts.sys.readFile).config,
ts.sys,
path.dirname(tsconfigPath)
);
const program: IExtendedProgram = ts.createProgram({
rootNames: parsed.fileNames,
options: parsed.options
}) as IExtendedProgram;

return program;
}

private async _ensureInitializedAsync(
taskSession: IHeftTaskSession,
heftConfiguration: HeftConfiguration
Expand Down