Skip to content

refactor(ci): enhance lint script with programmatic ESLint and line-level filtering#10238

Open
kevmoo wants to merge 3 commits intofirebase:mainfrom
kevmoo:lint_better
Open

refactor(ci): enhance lint script with programmatic ESLint and line-level filtering#10238
kevmoo wants to merge 3 commits intofirebase:mainfrom
kevmoo:lint_better

Conversation

@kevmoo
Copy link
Copy Markdown
Contributor

@kevmoo kevmoo commented Apr 1, 2026

Description

Refactored scripts/lint-changed-files.ts to use the programmatic ESLint API instead of spawning execSync child processes. This improves performance and allows for robust line-level filtering of lint errors.

  • Extracted monolithic main function into modular helpers: getChangedFiles, getChangedLines, runLint, reportStandard, and reportFiltered.
  • Unified the ESLint execution path.
  • Replaced inline process.exit with a custom LintError class handled at the top level.
  • Updated .github/workflows/node-test.yml to use --only-changed-lines by default.

Scenarios Tested

  • Ran standard mode to verify it reports all lints for modified files.
  • Ran --only-changed-lines mode to verify it filters out pre-existing warnings on unchanged lines.
  • Verified that syntax errors on changed lines are correctly reported.

Sample Commands

npm run lint:changed-files
npm run lint:changed-files -- --only-changed-lines

…evel filtering

### Description

Refactored `scripts/lint-changed-files.ts` to use the programmatic ESLint API instead of spawning `execSync` child processes. This improves performance and allows for robust line-level filtering of lint errors.

- Extracted monolithic `main` function into modular helpers: `getChangedFiles`, `getChangedLines`, `runLint`, `reportStandard`, and `reportFiltered`.
- Unified the ESLint execution path.
- Replaced inline `process.exit` with a custom `LintError` class handled at the top level.
- Updated `.github/workflows/node-test.yml` to use `--only-changed-lines` by default.

### Scenarios Tested

- Ran standard mode to verify it reports all lints for modified files.
- Ran `--only-changed-lines` mode to verify it filters out pre-existing warnings on unchanged lines.
- Verified that syntax errors on changed lines are correctly reported.

### Sample Commands

```bash
npm run lint:changed-files
npm run lint:changed-files -- --only-changed-lines
```
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the lint-changed-files.ts script to utilize the ESLint programmatic API and introduces a new --only-changed-lines flag for targeted linting. The review feedback highlights a potential buffer overflow risk in execSync when handling large diffs, a regression where ESLint CLI arguments are currently ignored, and a suggestion to clarify the success message when warnings are present.

@kevmoo
Copy link
Copy Markdown
Contributor Author

kevmoo commented Apr 1, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the lint-changed-files.ts script to use the programmatic ESLint API and introduces a new --only-changed-lines mode to filter linting results to only those lines modified in the current branch. Several issues were identified in the review: the new implementation ignores most ESLint CLI arguments (a regression), there is a potential command injection vulnerability when calling git diff, the manual formatting in reportFiltered should be replaced by the standard ESLint formatter for consistency, and the parsing of the --max-warnings flag is fragile and could lead to silent failures.

@kevmoo
Copy link
Copy Markdown
Contributor Author

kevmoo commented Apr 1, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the lint-changed-files.ts script to use the ESLint programmatic API and introduces a feature to filter linting results to only changed lines via the --only-changed-lines flag. It also adds support for --quiet and --max-warnings arguments. Feedback focuses on optimizing performance by combining multiple filter and reduce operations into single passes to avoid redundant array iterations. Additionally, a suggestion was made to ensure consistent error handling by throwing an error instead of calling process.exit directly, adhering to the repository's style guide.

Comment on lines +147 to +154
processedResults = results
.map((r) => ({
...r,
messages: r.messages.filter((m) => m.severity === 2),
errorCount: r.messages.filter((m) => m.severity === 2).length,
warningCount: 0,
}))
.filter((r) => r.messages.length > 0 || r.errorCount > 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The messages array is filtered twice to calculate the messages property and the errorCount. Additionally, the filter condition r.messages.length > 0 || r.errorCount > 0 is redundant since they are equivalent after the mapping. This can be optimized by filtering once.

    processedResults = results
      .map((r) => {
        const errors = r.messages.filter((m) => m.severity === 2);
        return {
          ...r,
          messages: errors,
          errorCount: errors.length,
          warningCount: 0,
        };
      })
      .filter((r) => r.errorCount > 0);

Comment on lines +161 to +168
const errorCount = processedResults.reduce(
(acc: number, r: EslintResult) => acc + r.errorCount,
0,
);
const warningCount = processedResults.reduce(
(acc: number, r: EslintResult) => acc + r.warningCount,
0,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Multiple reduce calls on the same array can be combined into a single pass for better efficiency.

  let errorCount = 0;
  let warningCount = 0;
  for (const r of processedResults) {
    errorCount += r.errorCount;
    warningCount += r.warningCount;
  }

Comment on lines +189 to +205
const filteredResults: EslintResult[] = results
.map((r) => {
const relPath = relative(root, r.filePath);
const changedLines = changedLinesByFile[relPath] || new Set<number>();
const messages = r.messages.filter((msg) => {
const lineMatch = changedLines.has(msg.line);
const quietMatch = !quiet || msg.severity === 2;
return lineMatch && quietMatch;
});
return {
...r,
messages,
errorCount: messages.filter((m) => m.severity === 2).length,
warningCount: messages.filter((m) => m.severity === 1).length,
};
})
.filter((r) => r.messages.length > 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The messages array is filtered multiple times within the map function. This can be optimized by calculating errorCount and warningCount during the initial filtering pass.

  const filteredResults: EslintResult[] = results
    .map((r) => {
      const relPath = relative(root, r.filePath);
      const changedLines = changedLinesByFile[relPath] || new Set<number>();
      let errorCount = 0;
      let warningCount = 0;
      const messages = r.messages.filter((msg) => {
        const lineMatch = changedLines.has(msg.line);
        const quietMatch = !quiet || msg.severity === 2;
        const keep = lineMatch && quietMatch;
        if (keep) {
          if (msg.severity === 2) errorCount++;
          else if (msg.severity === 1) warningCount++;
        }
        return keep;
      });
      return {
        ...r,
        messages,
        errorCount,
        warningCount,
      };
    })
    .filter((r) => r.messages.length > 0);

Comment on lines +213 to +214
const errorCount = filteredResults.reduce((acc, r) => acc + r.errorCount, 0);
const warningCount = filteredResults.reduce((acc, r) => acc + r.warningCount, 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

These reduce calls can be combined into a single pass.

Suggested change
const errorCount = filteredResults.reduce((acc, r) => acc + r.errorCount, 0);
const warningCount = filteredResults.reduce((acc, r) => acc + r.warningCount, 0);
let errorCount = 0;
let warningCount = 0;
for (const r of filteredResults) {
errorCount += r.errorCount;
warningCount += r.warningCount;
}

Comment on lines +257 to +258
console.error("Error: --max-warnings requires a numeric value.");
process.exit(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The PR description mentions replacing inline process.exit with a custom LintError handled at the top level. However, process.exit(1) is still used here. To maintain consistency and follow the repository's best practices regarding error handling, consider throwing an error instead.

Suggested change
console.error("Error: --max-warnings requires a numeric value.");
process.exit(1);
throw new Error("Error: --max-warnings requires a numeric value.");
References
  1. Throw errors for expected, user-facing errors instead of using process.exit directly. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants