[heft-lint-plugin] Lint files selected by ESLint flat config - #6006
[heft-lint-plugin] Lint files selected by ESLint flat config#6006Ian Clanton-Thuon (iclanton) wants to merge 6 commits into
Conversation
bca4d92 to
54eb97a
Compare
Use ESLint's native flat-config enumeration to find files outside the TypeScript program, then lint them through the existing cache and reporting pipeline. Exclude TypeScript emit folders and ESLint's built-in JavaScript extensions from the additional-file pass, partition SARIF metadata by ESLint instance, keep the TypeScript plugin as an accessor-only development dependency, and cover the behavior with the ESLint 9 SARIF fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scope the injected TypeScript program to the program's files so that a single ESLint instance lints both program files (using the in-memory program) and the additional files selected by the ESLint configuration (using the configuration's own parser), removing the separate additional-files linter. When a type-aware rule is applied to a file that is not part of the TypeScript program, emit actionable guidance to either exclude the file or lint it with a configuration that does not enable type-aware rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate the TypeScript program pattern computation into a single loop using Path.isUnder, replace localeCompare with comparison operators, read the additional files with a bounded Async.forEachAsync, drop redundant path resolution of ESLint's already-absolute paths, use Path.convertToSlashes, and clarify the enumerator configuration comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ation helper Group the profile's type-aware rules (naming-convention, no-floating-promises, no-for-in-array) into an explicit typeAwareRules set alongside the derived nonTypeAwareRules, and add a reusable flat/without-type-information helper that disables type-aware parsing and rules for a given set of files. Apply the same split to the repo's localCommonConfig and wire the helper through the node rigs, then use it in @rushstack/playwright-browser-tunnel to lint the Playwright config and test files (which are not part of the TypeScript program) with only the non-type-aware rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iles Now that the Playwright config and test files are linted (with non-type-aware rules), fix the issues they surface: add the license header to playwright.config.ts, and in tests/testFixture.ts correct the import to the exported createTunneledBrowserAsync API, add the missing type annotations, order the imports, and use an allowed console method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0cb1dd4 to
2f28a5b
Compare
- heft-lint-plugin: resolve TypeScript program root file names against the project folder (not the process cwd), and derive the project-relative ESLint patterns by slicing the project-folder prefix instead of using path.relative. - @rushstack/eslint-config: drop the derived nonTypeAwareRules export/loop and keep only the explicit typeAwareRules group; simplify the without-type-information helper to an explicit disabled-rules list. - Defer the decoupled-local-node-rig / node-rig wiring to a follow-up PR (to be done after @rushstack/eslint-config is published and the dependency is bumped); fix @rushstack/playwright-browser-tunnel inline in its own eslint.config.js for now. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| ...nodeTrustedToolProfile, | ||
| ...friendlyLocalsMixin, | ||
| { | ||
| files: ['**/*.ts', '**/*.tsx'], |
There was a problem hiding this comment.
Shouldn't the files covered by withoutTypeInformation be excluded from this?
There was a problem hiding this comment.
(COPILOT) The rig helper moved to a follow-up PR; I now disable type-aware linting for those files inline instead. That block is applied after the **/*.ts block, so it overrides project to false for playwright.config.ts/tests/** — the earlier tsconfigRootDir is harmless for them (it only matters once a project is loaded). Happy to add them to the earlier block's ignores if you'd prefer an explicit exclusion.
| // Derive the non-type-aware rule group from the authored TypeScript source-file config so that it stays in sync | ||
| // with the rules above. Everything that is not part of typeAwareRules can be applied to files that lack type | ||
| // information. | ||
| const typeScriptSourceFileConfig = commonConfig.find( | ||
| (configObject) => | ||
| Array.isArray(configObject.files) && configObject.files.includes('**/*.ts') && configObject.rules | ||
| ); | ||
| const nonTypeAwareRules = {}; | ||
| for (const [ruleName, ruleValue] of Object.entries(typeScriptSourceFileConfig.rules)) { | ||
| if (!(ruleName in typeAwareRules)) { | ||
| nonTypeAwareRules[ruleName] = ruleValue; | ||
| } | ||
| } |
There was a problem hiding this comment.
Seems inappropriate to loop over a data structure that we just created and fully control. Can we refactor to get rid of this?
Also, it would be useful to convert the JS files in this project to TS and introduce a build in this project. We can maintain imports using the "exports" package.json property.
There was a problem hiding this comment.
(COPILOT) Done — removed the derived nonTypeAwareRules (and its loop/export). The profile now exposes only the explicit typeAwareRules group, spread into the config. (Noted the JS→TS conversion idea as a separate improvement.)
| for (const ruleName of [...Object.keys(typeAwareRules), ...additionalTypeAwareRuleNames]) { | ||
| disabledTypeAwareRules[ruleName] = 'off'; | ||
| } |
There was a problem hiding this comment.
Wouldn't it be cleaner to just separate these into independent lists?
There was a problem hiding this comment.
(COPILOT) Simplified — the helper now uses an explicit disabledTypeAwareRules list and no longer merges/loops over rule names. The additionalTypeAwareRuleNames parameter is gone now that the rig wiring is a follow-up PR.
| this.#sarifLogPath = sarifLogPath; | ||
|
|
||
| this.#typeScriptFilenames = new Set( | ||
| tsProgram.getRootFileNames().map((filePath: string) => path.resolve(filePath)) |
There was a problem hiding this comment.
Are these files not already resolved? We also don't know if the cwd is the project root/tsconfig folder.
There was a problem hiding this comment.
(COPILOT) Fixed — root file names are now resolved against buildFolderPath (path.resolve(buildFolderPath, filePath)) rather than the process cwd.
| const typeScriptFilePatterns: string[] = []; | ||
| for (const filePath of this.#typeScriptFilenames) { | ||
| if (Path.isUnder(filePath, buildFolderPath)) { | ||
| typeScriptFilePatterns.push(Path.convertToSlashes(path.relative(buildFolderPath, filePath))); |
There was a problem hiding this comment.
Can we avoid path.relative here? Can't we just cut off the number of characters in buildFolderPath if filePath is under it?
There was a problem hiding this comment.
(COPILOT) Done — since Path.isUnder(filePath, buildFolderPath) is already checked, the pattern is now filePath.slice(buildFolderPath.length + 1) then Path.convertToSlashes(...), with no path.relative.
| const additionalFilePaths: string[] = []; | ||
| for (const { filePath } of lintResults) { | ||
| if ( | ||
| !this.#typeScriptFilenames.has(filePath) && |
There was a problem hiding this comment.
Seems like this should get passed in as a parameter instead of assuming that this class property will be already defined.
There was a problem hiding this comment.
(COPILOT) #typeScriptFilenames is assigned in the constructor before any method can run, so it is always defined here; it is also used by the constructor's program scoping and the error path, which is why it is a field. I can thread it through as a parameter if you'd rather not rely on the field.
There was a problem hiding this comment.
Yeah, pass it as a parameter.
| /** | ||
| * Files selected by the linter configuration that are not part of the TypeScript program. | ||
| */ | ||
| additionalFiles?: ReadonlySet<TAdditionalLintFile>; |
There was a problem hiding this comment.
Why do we care about this outside of ESLint?
There was a problem hiding this comment.
(COPILOT) It is generic on LinterBase<TLintResult, TAdditionalLintFile = never>, so TSLint (and any linter without additional files) neither sees nor handles it — only ESLint sets the second type parameter. performLintingAsync just folds them into the file set/cache alongside the program files. I can push it entirely into the ESLint subclass if you'd prefer the base stay unaware of it.
|
|
||
| const { buildFolderPath } = heftConfiguration; | ||
| return Array.from(outputFolderPaths, (outputFolderPath: string) => { | ||
| const relativePath: string = Path.convertToSlashes(path.relative(buildFolderPath, outputFolderPath)); |
There was a problem hiding this comment.
Same questions about file paths already being normalized/relative.
There was a problem hiding this comment.
(COPILOT) Reworked the same way — output folders are resolved against buildFolderPath, and for those under it the pattern is built by slicing the prefix (guarded by Path.isUnder) instead of path.relative + filtering.
|
|
||
| const nodeImportResolverPath = require.resolve('eslint-import-resolver-node'); | ||
|
|
||
| // These localCommonConfig rules require type information (i.e. the TypeScript program). They are grouped |
There was a problem hiding this comment.
These changes can probably all go away when @rushstack/eslint-config is published and pulled back in, right? I'm thinking we just make that its own PR and plan on doing this after the dependency of this project gets bumped.
There was a problem hiding this comment.
(COPILOT) Agreed — reverted the localTypeAwareRules split and the rig without-type-information wrappers out of this PR. I'll do them in a follow-up once @rushstack/eslint-config is published and this project's dependency is bumped, so the rig can consume the published typeAwareRules/helper directly. For now @rushstack/playwright-browser-tunnel is fixed inline in its own eslint.config.js.
| @@ -1 +1 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | |||
There was a problem hiding this comment.
We should consider similarly converting this project to TypeScript.
There was a problem hiding this comment.
(COPILOT) Noted — I'll leave the TS conversion of this rig project for a separate change.
| additionalFilePaths.sort((left: string, right: string) => { | ||
| if (left < right) { | ||
| return -1; | ||
| } else if (left > right) { | ||
| return 1; | ||
| } else { | ||
| return 0; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Can't we just say additionalFilePaths.sort()?
Summary
The Heft lint plugin currently sends ESLint only the files from the TypeScript program. As a result, files selected exclusively by the ESLint flat config — for example configuration files, tests excluded from
tsconfig.json, or custom extensions such as Markdown or JSON — are never linted.This change uses ESLint's native flat-config enumeration to discover those files and lints them through the plugin's existing cache, fix, diagnostic, and SARIF pipeline.
Details
For ESLint 9, the plugin creates a discovery-only ESLint instance and calls
lintFiles()with rules disabled to enumerate the files the flat config selects. TypeScript program roots and primary TypeScript emit directories are excluded from discovery. ESLint's built-in JavaScript extensions (.js,.mjs,.cjs) are excluded from the additional-file pass because ESLint enumerates them even when the flat config did not introduce them; this also prevents build outputs produced by other Heft tasks from becoming new lint inputs. Extensions introduced by the flat config (Markdown, JSON, JSX, custom languages,.tsfiles outside the program, etc.) remain discoverable.A single ESLint instance lints both the program files and the discovered additional files. The injected TypeScript
Programis scoped (via afiles-limited override) to the program's own files, so additional files fall through to the flat config's own parser instead of failing to resolve against the program. Additional files are run through the existing per-file content/config cache and result reporting, and SARIF metadata is collected from that instance. In builds with multiple TypeScript programs, additional files are included only once.When a type-aware rule would apply to a file that is not part of the TypeScript program (typescript-eslint cannot produce type information for it), the plugin now reports an actionable error telling the user to either exclude the file from ESLint or lint it with a configuration that does not enable type-aware rules.
ESLint 8 and TSLint behavior is unchanged.
@rushstack/eslint-config: separating type-aware rulesTo make the above easy to adopt,
@rushstack/eslint-confignow groups its type-aware rules (naming-convention,no-floating-promises,no-for-in-array) into an explicittypeAwareRulesset alongside the derivednonTypeAwareRules(the combined rule set is unchanged), and adds aflat/without-type-informationhelper. The helper disables type-aware parsing and the type-aware rules for a given set of files, so files outside the TypeScript program can be linted with only the non-type-aware rules. The same split is applied to the repo's internallocalCommonConfigand wired through the node rigs.@rushstack/playwright-browser-tunneldemonstrates the pattern: itsplaywright.config.tsandtests/**files are excluded fromtsconfig.jsonand are now linted with only the non-type-aware rules viawithoutTypeInformation(...). This surfaced — and this PR fixes — several previously-unlinted issues in those files (including a broken import in the test fixture).How it was tested
heft buildinheft-plugins/heft-lint-plugin(TypeScript + self-lint + API Extractor): passedheft test --cleaninbuild-tests/eslint-9-test: 1 passed, including a non-TypeScript custom-extension SARIF resultheft build --cleaninapps/playwright-browser-tunnel: passes, linting the Playwright config and test files with only the non-type-aware rulesrush rebuildfor@rushstack/heft-lint-plugin(local rig) and@rushstack/eslint-plugin(decoupled rig): no lint regressions from the config splitrush change --verify/git diff --checkEarlier manual verification (on the pre-rebase feature commit) also confirmed that generated
dist/**/*.jsoutputs inlibraries/rush-libandbuild-tests/localization-plugin-test-02are not picked up as lint inputs.