Skip to content

[heft-lint-plugin] Lint files selected by ESLint flat config - #6006

Open
Ian Clanton-Thuon (iclanton) wants to merge 9 commits into
microsoft:mainfrom
iclanton:fix/heft-lint-flat-config-files
Open

Ian Clanton-Thuon (iclanton) wants to merge 9 commits into
microsoft:mainfrom
iclanton:fix/heft-lint-flat-config-files

Conversation

@iclanton

@iclanton Ian Clanton-Thuon (iclanton) commented Sep 2, 2026

Copy link
Copy Markdown
Member

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, authored .js scripts, 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 with a single ESLint instance.

Note: The @rushstack/eslint-config changes this feature builds on (grouping the type-aware rules and adding the flat/without-type-information helper, plus the TypeScript conversion of that package) landed separately in #6026 and are already published. This PR now depends on that published version.

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 are excluded from discovery. Everything else the flat config selects — authored .js/.mjs/.cjs, config files, .ts files outside the program, and custom extensions (Markdown, JSON, JSX, etc.) — is discoverable.

A single ESLint instance lints both the program files and the discovered additional files. The injected TypeScript Program is scoped (via a files-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.

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 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.

Ignoring build output

Because the enumeration now lints authored JavaScript (not only TypeScript sources), ESLint's lintFiles('.') would otherwise walk into generated output, which ESLint's flat config does not exclude (it does not respect .gitignore). Rather than blanket-ignoring .js/.mjs/.cjs (which would also prevent linting authored JavaScript), build output is ignored explicitly:

  • @rushstack/eslint-config now globally ignores lib, lib-*, dist, temp, and coverage (anchored to the project root).
  • decoupled-local-node-rig repeats the same global ignores so projects consuming the currently-published @rushstack/eslint-config via that rig also get them (to be removed once the dependency is bumped).
  • A few projects add ignores for non-standard generated/fixture folders: package-extractor (test-output), the Rush redis cobuild integration test (sandbox), and the Rush VS Code extension (webview bundle output).

Adopting the pattern in this repo

@rushstack/eslint-config (published via #6026) exposes a flat/without-type-information helper that disables type-aware parsing and rules for a given set of files. This PR wires that helper through the repo's node rigs (decoupled-local-node-rig and local-node-rig, including the rig's own localTypeAwareRules group), and uses it in @rushstack/playwright-browser-tunnel to lint its playwright.config.ts and tests/** files — which are excluded from the TypeScript program — with only the non-type-aware rules. This also surfaces and fixes several previously-unlinted issues in those files (including a broken import in the test fixture).

How it was tested

  • heft build in heft-plugins/heft-lint-plugin (TypeScript + self-lint + API Extractor): passed
  • heft test --clean in build-tests/eslint-9-test: passes, including a non-TypeScript custom-extension result and an authored eslint.config.js in the SARIF snapshot, while emitted lib-commonjs/lib-esm output is excluded
  • heft build --clean in apps/playwright-browser-tunnel: passes, linting the Playwright config and test files with only the non-type-aware rules via the rig helper
  • A full rush build (the shared-config change re-lints every project): passes; generated build output is not linted, authored .js is
  • File paths are normalized to forward slashes so program-file matching works on Windows (CI is green on Windows and Ubuntu)
  • rush change --verify

@github-project-automation github-project-automation Bot moved this to Needs triage in Bug Triage Sep 2, 2026
@iclanton
Ian Clanton-Thuon (iclanton) force-pushed the fix/heft-lint-flat-config-files branch 2 times, most recently from bca4d92 to 54eb97a Compare September 5, 2026 00:05
Comment thread heft-plugins/heft-lint-plugin/package.json Outdated
Comment thread apps/playwright-browser-tunnel/eslint.config.js
Comment thread eslint/eslint-config/flat/profile/_common.js Outdated
Comment thread eslint/eslint-config/flat/without-type-information.js Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/LinterBase.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/LintPlugin.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/Eslint.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/LinterBase.ts Outdated
Comment thread heft-plugins/heft-lint-plugin/src/LintPlugin.ts Outdated
Use ESLint's native flat-config enumeration to find files outside the TypeScript
program and lint them through the existing cache and reporting pipeline with a
single ESLint instance. 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.

Also fix the lint issues this surfaces in @rushstack/playwright-browser-tunnel's
Playwright config and test fixture files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that @rushstack/eslint-config is published with the `flat/without-type-information`
helper, group the rig's own type-aware rules (localCommonConfig) into a
`localTypeAwareRules` set and expose a `without-type-information` helper from
`decoupled-local-node-rig` and `local-node-rig` that disables type-aware parsing
plus both the base profile's and the rig's type-aware rules. Use it in
@rushstack/playwright-browser-tunnel to lint the Playwright config and test files,
which are excluded from the TypeScript program.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify the additional-file sort to a default lexicographic sort, note that the
enumerator lints relative to `buildFolderPath` (its `cwd`), convert the type-info
error helper to a loose function that takes the TypeScript file set and build
folder as parameters, and empty the playwright-browser-tunnel change comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review feedback:
- Remove the `additionalFiles` option and the extra-file generic from LinterBase;
  the base now exposes a neutral `getExtraSourceFilesToLintAsync` hook (default
  empty) that Eslint overrides to enumerate the files selected by the ESLint
  configuration. The base only deals with a generic `ISourceFileToLint` shape.
- Thread the (project-folder-resolved) TypeScript file set into the enumeration
  as a parameter instead of reading it from a field, resolving the paths at the
  LintPlugin call site.
- Make `ISourceFileToLint.version` optional (omit it for enumerated files) so the
  base computes the version from file contents, instead of passing an empty string.
- Drop the redundant TypeScript-output-folder ignore patterns; emitted JavaScript
  is already excluded by the default-JavaScript-extension filter (and emit folders
  such as `lib-esm` cannot be derived from the compiler options anyway).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of blanket-ignoring ESLint's default JavaScript extensions (which also
prevented authored `.js` files from being linted), discover the TypeScript output
folders and ignore only those when enumerating additional files.

- heft-typescript-plugin: expose `emitFolderPaths` on `IChangedFilesHookOptions`
  (the `outDir`/`declarationDir` plus any `additionalModuleKindsToEmit` folders,
  such as `lib-esm`, which are not part of the compiler options).
- heft-lint-plugin: aggregate those folders from the accessor and ignore them in
  the additional-file enumerator, and remove the default-JavaScript-extension
  filter so that authored `.js` files selected by the ESLint configuration are
  linted. Update the eslint-9-test snapshot, which now lints `eslint.config.js`
  while still excluding the emitted `lib-commonjs`/`lib-esm` output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… by extension

Now that the additional-file enumeration lints authored JavaScript (not just
TypeScript sources), ESLint's `lintFiles('.')` would walk into generated output,
which ESLint's flat config does not exclude (it does not respect `.gitignore`).
Rather than blanket-ignoring `.js`/`.mjs`/`.cjs`, ignore build output explicitly:

- @rushstack/eslint-config: globally ignore `lib`, `lib-*`, `dist`, `temp`, and
  `coverage` (anchored to the project root).
- decoupled-local-node-rig: repeat the same global ignores so projects consuming
  the currently-published @rushstack/eslint-config via this rig also get them
  (to be removed once that dependency is bumped).
- Add project-level ignores for non-standard generated/fixture folders:
  package-extractor `test-output`, rush-redis-cobuild sandbox `sandbox`, and the
  rush vscode extension `webview` bundle output.
- Revert the `emitFolderPaths` TypeScript-plugin accessor addition (and its
  consumption), since the explicit config ignores cover the build output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The additional-file feature resolved the TypeScript program file names with
`path.resolve(buildFolderPath, ...)`, which produces backslash paths on Windows.
LinterBase then compared those against `SourceFile.fileName` (which TypeScript
always reports with forward slashes), so on Windows every program file missed the
lookup and was skipped -- producing empty lint results for the program files
(observed as an eslint-9-test SARIF snapshot mismatch on Windows).

Normalize all file paths to forward slashes before comparing them: the resolved
TypeScript program file names, the paths ESLint reports for enumerated additional
files, and the project-folder prefix used to compute the ignore patterns. This is
a no-op on POSIX and corrects the comparison on Windows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…patterns

Fixes two issues found in review of the additional-file enumeration:

- Multi-program double-processing: with project-reference / composite builds the
  lint hook receives more than one TypeScript program, but additional-file
  enumeration ran only for the first program and excluded only that program's
  files. Files belonging to other programs were then linted both as additional
  files (in the first pass) and as program files (in their own pass). Exclude the
  union of every program's root file names from the enumeration, and de-duplicate
  the linted files in LinterBase so a file that is both a program source file and
  an enumerated file is linted only once.
- Glob metacharacters: exact program file paths were used directly as ESLint
  `files`/`ignores` patterns, so a file name containing characters such as
  `[ ] * ? { } ( )` would be interpreted as a glob. Escape those characters when
  building the patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous escaping was not compatible with the minimatch version ESLint uses
(3.1.5, with `{ dot: true, allowWindowsEscape: true }`): escaping `@`/`+` broke
matching of names like `foo@(bar).ts`, and a leading `#` (comment) or `!`
(negation) was not handled. Escape only the always-significant characters
(`\ * ? [ ] { } ( )`) -- escaping the parentheses already neutralizes extglob
prefixes -- and escape a leading `#`/`!`. Verified against ESLint's minimatch
options that ordinary and pathological file names match exactly with no
over-broadening.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant