diff --git a/.changeset/rotten-wombats-cry.md b/.changeset/rotten-wombats-cry.md new file mode 100644 index 0000000000..d9817fbace --- /dev/null +++ b/.changeset/rotten-wombats-cry.md @@ -0,0 +1,7 @@ +--- +"@redocly/cli": minor +--- + +Changed `lint` behavior with the `--generate-ignore-file` option. +Now `lint` updates only the entries related to the file being linted. +Other files' entries are unchanged. diff --git a/docs/@v2/commands/lint.md b/docs/@v2/commands/lint.md index 0135402116..5dd93dc239 100644 --- a/docs/@v2/commands/lint.md +++ b/docs/@v2/commands/lint.md @@ -352,7 +352,8 @@ This option is useful when you have an API design standard, but have some except It allows for highly granular control. {% admonition type="warning" %} -This command overwrites an existing ignore file. +This command updates ignore entries only for the API descriptions passed to the command. +Existing entries for other API descriptions are preserved. {% /admonition %} The following command runs `lint` and adds all the errors to an ignore file: @@ -369,6 +370,15 @@ Generated ignore file with 3 problems. The errors in the ignore file `.redocly.lint-ignore.yaml` are ignored when the `lint` command is run. +If you run: + +```bash +redocly lint api1.yaml --generate-ignore-file +``` + +Only the ignore entries related to api1.yaml are updated. +Ignore entries for other API descriptions remain unchanged. + To generate an ignore file for multiple API descriptions, pass them as arguments: ```bash diff --git a/packages/cli/src/__tests__/commands/lint.test.ts b/packages/cli/src/__tests__/commands/lint.test.ts index f91469d653..1106f38f0a 100644 --- a/packages/cli/src/__tests__/commands/lint.test.ts +++ b/packages/cli/src/__tests__/commands/lint.test.ts @@ -8,6 +8,7 @@ import { loadConfig, } from '@redocly/openapi-core'; import { blue } from 'colorette'; +import { resolve } from 'node:path'; import { performance } from 'perf_hooks'; import { type MockInstance } from 'vitest'; import { type Arguments } from 'yargs'; @@ -114,7 +115,7 @@ describe('handleLint', () => { ); }); - it('should call mergedConfig with clear ignore if `generate-ignore-file` argv', async () => { + it('should call forAlias when `generate-ignore-file` argv is set', async () => { await commandWrapper(handleLint)({ ...argvMock, 'generate-ignore-file': true }); expect(configFixture.forAlias).toHaveBeenCalledWith(undefined); }); @@ -151,6 +152,64 @@ describe('handleLint', () => { expect(configFixture.skipPreprocessors).toHaveBeenCalledWith(['preprocessor']); }); + it('should update only the linted file entries and preserve ignore entries for other files', async () => { + const barAbsRef = resolve('ignore-bar.yaml'); + const fooAbsRef = resolve('ignore-foo.yaml'); + + configFixture.ignore = { + [barAbsRef]: { + 'no-empty-servers': new Set(['#/servers']), + 'operation-summary': new Set(['#/paths/~1items/get/summary']), + }, + [fooAbsRef]: { + 'no-empty-servers': new Set(['#/servers']), + }, + }; + + vi.mocked(configFixture.clearIgnoreForRef).mockImplementation((ref: string) => { + const absRef = resolve(ref); + delete configFixture.ignore[absRef]; + }); + + vi.mocked(configFixture.addIgnore).mockImplementation((problem: any) => { + const loc = problem.location[0]; + if (loc.pointer === undefined) return; + const fileIgnore = (configFixture.ignore[loc.source.absoluteRef] = + configFixture.ignore[loc.source.absoluteRef] || {}); + const ruleIgnore = (fileIgnore[problem.ruleId] = fileIgnore[problem.ruleId] || new Set()); + ruleIgnore.add(loc.pointer); + }); + + vi.mocked(lint).mockResolvedValueOnce([ + { + ruleId: 'no-empty-servers', + severity: 1, + message: 'Servers must not be empty', + location: [{ source: { absoluteRef: barAbsRef }, pointer: '#/servers' }], + suggest: [], + ignored: false, + } as any, + ]); + + await commandWrapper(handleLint)({ + ...argvMock, + apis: ['ignore-bar.yaml'], + 'generate-ignore-file': true, + }); + + expect(configFixture.ignore[fooAbsRef]).toEqual({ + 'no-empty-servers': new Set(['#/servers']), + }); + + expect(configFixture.ignore[barAbsRef]).toEqual({ + 'no-empty-servers': new Set(['#/servers']), + }); + + expect(configFixture.saveIgnore).toHaveBeenCalled(); + + configFixture.ignore = {}; + }); + it('should call formatProblems and getExecutionTime with argv', async () => { vi.mocked(lint).mockResolvedValueOnce(['problem'] as any); await commandWrapper(handleLint)({ ...argvMock, 'max-problems': 2, format: 'stylish' }); diff --git a/packages/cli/src/__tests__/fixtures/config.ts b/packages/cli/src/__tests__/fixtures/config.ts index bf22eaa163..2611e20238 100644 --- a/packages/cli/src/__tests__/fixtures/config.ts +++ b/packages/cli/src/__tests__/fixtures/config.ts @@ -11,6 +11,7 @@ export const configFixture: Config = { }, configPath: undefined, addIgnore: vi.fn(), + clearIgnoreForRef: vi.fn(), skipRules: vi.fn(), skipPreprocessors: vi.fn(), saveIgnore: vi.fn(), diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 1ecf062c03..d0dc4a5c63 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -51,9 +51,6 @@ export async function handleLint({ exitWithError('No APIs were provided.'); } - if (argv['generate-ignore-file']) { - config.ignore = {}; // clear ignore - } const totals: Totals = { errors: 0, warnings: 0, ignored: 0 }; let totalIgnored = 0; @@ -95,6 +92,7 @@ export async function handleLint({ totals.ignored += fileTotals.ignored; if (argv['generate-ignore-file']) { + config.clearIgnoreForRef(path); for (const m of results) { config.addIgnore(m); totalIgnored++; @@ -118,9 +116,7 @@ export async function handleLint({ if (argv['generate-ignore-file']) { config.saveIgnore(); - logger.info( - `Generated ignore file with ${totalIgnored} ${pluralize('problem', totalIgnored)}.\n\n` - ); + logger.info(`Explicitly ignored ${totalIgnored} ${pluralize('problem', totalIgnored)}.\n\n`); } else { printLintTotals(totals, apis.length); } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 837525353f..7ef6647bbe 100755 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -166,6 +166,12 @@ export class Config { ); } + clearIgnoreForRef(ref: string) { + const dir = this.configPath ? path.dirname(this.configPath) : process.cwd(); + const absRef = isAbsoluteUrl(ref) ? ref : path.resolve(dir, ref); + delete this.ignore[absRef]; + } + saveIgnore() { const dir = this.configPath ? path.dirname(this.configPath) : process.cwd(); const ignoreFile = path.join(dir, IGNORE_FILE);