Skip to content
Merged
7 changes: 7 additions & 0 deletions .changeset/rotten-wombats-cry.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion docs/@v2/commands/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
61 changes: 60 additions & 1 deletion packages/cli/src/__tests__/commands/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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' });
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/__tests__/fixtures/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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++;
Expand All @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading