Skip to content

Commit 57fcb39

Browse files
authored
fix: issue with .redocly.lint-ignore.yaml (#2512)
1 parent b1767fe commit 57fcb39

14 files changed

Lines changed: 84 additions & 204 deletions

File tree

.changeset/fine-buttons-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@redocly/openapi-core": patch
3+
---
4+
5+
Revert: fixed an issue where `.redocly.lint-ignore.yaml` was not loaded in browser environments.

packages/core/src/__tests__/lint.test.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'node:path';
22
import { outdent } from 'outdent';
33
import { lintFromString, lintConfig, lintDocument, lint } from '../lint.js';
44
import { BaseResolver } from '../resolve.js';
5-
import { createConfig, loadConfig, loadIgnoreFile } from '../config/load.js';
5+
import { createConfig, loadConfig } from '../config/load.js';
66
import { parseYamlToDocument, replaceSourceWithRef } from '../../__tests__/utils.js';
77
import { detectSpec } from '../detect-spec.js';
88
import {
@@ -1712,20 +1712,15 @@ describe('lint', () => {
17121712
);
17131713

17141714
const configFilePath = path.join(__dirname, 'fixtures');
1715-
const resolver = new BaseResolver();
1716-
const ignoreResult = await loadIgnoreFile(configFilePath, resolver);
17171715

17181716
const result = await lintDocument({
1719-
externalRefResolver: resolver,
1717+
externalRefResolver: new BaseResolver(),
17201718
document,
17211719
config: await createConfig(
17221720
{
17231721
rules: { 'operation-operationId': 'error' },
17241722
},
1725-
{
1726-
configPath: configFilePath,
1727-
ignoreFile: ignoreResult,
1728-
}
1723+
{ configPath: configFilePath }
17291724
),
17301725
});
17311726
expect(result).toHaveLength(1);

packages/core/src/__tests__/ref-utils.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import {
55
parseRef,
66
refBaseName,
77
unescapePointerFragment,
8-
isAbsoluteUrl,
9-
getDir,
10-
resolvePath,
118
} from '../ref-utils.js';
129
import { lintDocument } from '../lint.js';
1310
import { createConfig } from '../config/index.js';
@@ -186,50 +183,4 @@ describe('ref-utils', () => {
186183
expect(unescapePointerFragment('scope~1complex~0name')).toStrictEqual('scope/complex~name');
187184
});
188185
});
189-
190-
describe('isAbsoluteUrl', () => {
191-
it('should return true for http://, https://, and file:// URLs', () => {
192-
expect(isAbsoluteUrl('http://example.com/api.yaml')).toBe(true);
193-
expect(isAbsoluteUrl('https://example.com/api.yaml')).toBe(true);
194-
expect(isAbsoluteUrl('file:///Users/test/api.yaml')).toBe(true);
195-
});
196-
197-
it('should return false for relative and absolute file paths', () => {
198-
expect(isAbsoluteUrl('./api.yaml')).toBe(false);
199-
expect(isAbsoluteUrl('../api.yaml')).toBe(false);
200-
expect(isAbsoluteUrl('/Users/test/api.yaml')).toBe(false);
201-
});
202-
});
203-
204-
describe('getDir', () => {
205-
it('should return directory for file paths and URLs', () => {
206-
expect(getDir('/Users/test/config/redocly.yaml')).toBe('/Users/test/config');
207-
expect(getDir('http://example.com/config/redocly.yaml')).toBe('http://example.com/config');
208-
expect(getDir('https://example.com/config/redocly.yaml')).toBe('https://example.com/config');
209-
expect(getDir('file:///Users/test/config/redocly.yaml')).toBe('file:///Users/test/config');
210-
});
211-
212-
it('should return path as-is if no extension (directory)', () => {
213-
expect(getDir('/Users/test/config')).toBe('/Users/test/config');
214-
expect(getDir('file:///Users/test/config')).toBe('file:///Users/test/config');
215-
});
216-
});
217-
218-
describe('resolvePath', () => {
219-
it('should resolve paths for URLs', () => {
220-
expect(resolvePath('http://example.com/config', 'file.yaml')).toBe(
221-
'http://example.com/config/file.yaml'
222-
);
223-
expect(resolvePath('https://example.com/config/', 'file.yaml')).toBe(
224-
'https://example.com/config/file.yaml'
225-
);
226-
expect(resolvePath('file:///Users/test/config', 'file.yaml')).toBe(
227-
'file:///Users/test/config/file.yaml'
228-
);
229-
});
230-
231-
it('should resolve relative paths for file system paths', () => {
232-
expect(resolvePath('/Users/test/config', 'file.yaml')).toMatch(/file\.yaml$/);
233-
});
234-
});
235186
});

packages/core/src/config/__tests__/config.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import { type SpecVersion } from '../../oas-types.js';
22
import { Config } from '../config.js';
3+
import * as jsYaml from '../../js-yaml/index.js';
4+
import * as fs from 'node:fs';
5+
import { ignoredFileStub } from './fixtures/ingore-file.js';
6+
import * as path from 'node:path';
37
import { createConfig } from '../index.js';
8+
import * as doesYamlFileExistModule from '../../utils/does-yaml-file-exist.js';
9+
10+
vi.mock('../../js-yaml/index.js', async () => {
11+
const actual = await vi.importActual('../../js-yaml/index.js');
12+
return { ...actual };
13+
});
14+
vi.mock('node:fs', async () => {
15+
const actual = await vi.importActual('node:fs');
16+
return { ...actual };
17+
});
18+
vi.mock('node:path', async () => {
19+
const actual = await vi.importActual('node:path');
20+
return { ...actual };
21+
});
422

523
// Create the config and clean up not needed props for consistency
624
const testConfig: Config = await createConfig(
@@ -219,16 +237,12 @@ describe('Config.extendTypes', () => {
219237

220238
describe('generation ignore object', () => {
221239
it('should generate config with absoluteUri for ignore', () => {
222-
const ignore = {
223-
'some-path/openapi.yaml': {
224-
'no-unused-components': new Set(['#/components/schemas/Foo']),
225-
},
226-
'https://some-path.yaml': {
227-
'no-unused-components': new Set(['#/components/schemas/Foo']),
228-
},
229-
};
240+
vi.spyOn(fs, 'readFileSync').mockImplementationOnce(() => '');
241+
vi.spyOn(jsYaml, 'parseYaml').mockImplementationOnce(() => ignoredFileStub);
242+
vi.spyOn(doesYamlFileExistModule, 'doesYamlFileExist').mockImplementationOnce(() => true);
243+
vi.spyOn(path, 'resolve').mockImplementationOnce((_, filename) => `some-path/${filename}`);
230244

231-
const config = new Config(testConfig.resolvedConfig, { ignore });
245+
const config = new Config(testConfig.resolvedConfig);
232246
config.resolvedConfig = 'resolvedConfig stub' as any;
233247

234248
expect(config).toMatchSnapshot();

packages/core/src/config/__tests__/fixtures/ignore-file/.redocly.lint-ignore.yaml

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/core/src/config/__tests__/fixtures/ignore-file/api.yaml

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/core/src/config/__tests__/fixtures/ignore-file/redocly.yaml

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const ignoredFileStub = {
2+
'openapi.yaml': {
3+
'no-unused-components': ['#/components/schemas/Foo'],
4+
},
5+
'https://some-path.yaml': {
6+
'no-unused-components': ['#/components/schemas/Foo'],
7+
},
8+
};

packages/core/src/config/__tests__/load.test.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,34 +1344,3 @@ function verifyOasRules(
13441344
}
13451345
});
13461346
}
1347-
1348-
describe('loadIgnoreFile', () => {
1349-
const ignoreFileDir = path.join(__dirname, './fixtures/ignore-file');
1350-
const ignoreFileConfig = path.join(ignoreFileDir, 'redocly.yaml');
1351-
const expectedIgnoreKey = path.join(ignoreFileDir, 'api.yaml');
1352-
1353-
it('should ignore only rules specified in ignore file', async () => {
1354-
const config = await loadConfig({ configPath: ignoreFileConfig });
1355-
1356-
expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);
1357-
expect(config.ignore[expectedIgnoreKey]['operation-operationId']).toBeInstanceOf(Set);
1358-
expect(config.ignore[expectedIgnoreKey]['operation-summary']).toBeUndefined();
1359-
});
1360-
1361-
it('should return empty object when ignore file does not exist', async () => {
1362-
const configPath = path.join(__dirname, './fixtures/load-redocly.yaml');
1363-
const config = await loadConfig({ configPath });
1364-
1365-
expect(config.ignore).toEqual({});
1366-
});
1367-
1368-
it('should load ignore file in browser environment (without fs.existsSync)', async () => {
1369-
const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(undefined as any);
1370-
1371-
const config = await loadConfig({ configPath: ignoreFileConfig });
1372-
1373-
expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);
1374-
1375-
existsSyncSpy.mockRestore();
1376-
});
1377-
});

packages/core/src/config/config.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as fs from 'node:fs';
22
import * as path from 'node:path';
3-
import { stringifyYaml } from '../js-yaml/index.js';
3+
import { parseYaml, stringifyYaml } from '../js-yaml/index.js';
44
import { slash } from '../utils/slash.js';
5+
import { doesYamlFileExist } from '../utils/does-yaml-file-exist.js';
56
import { isPlainObject } from '../utils/is-plain-object.js';
67
import { specVersions } from '../detect-spec.js';
8+
import { isBrowser } from '../env.js';
79
import { getResolveConfig } from './get-resolve-config.js';
8-
import { isAbsoluteUrl, resolvePath } from '../ref-utils.js';
10+
import { isAbsoluteUrl } from '../ref-utils.js';
911
import { groupAssertionRules } from './group-assertion-rules.js';
1012
import { IGNORE_BANNER, IGNORE_FILE } from './constants.js';
1113

@@ -31,10 +33,18 @@ import type {
3133
ResolvedConfig,
3234
RuleConfig,
3335
RuleSettings,
34-
IgnoreFile,
35-
ResolvedIgnore,
3636
} from './types.js';
3737

38+
function getIgnoreFilePath(configPath?: string): string | undefined {
39+
if (configPath) {
40+
return doesYamlFileExist(configPath)
41+
? path.join(path.dirname(configPath), IGNORE_FILE)
42+
: path.join(configPath, IGNORE_FILE);
43+
} else {
44+
return isBrowser ? undefined : path.join(process.cwd(), IGNORE_FILE);
45+
}
46+
}
47+
3848
export class Config {
3949
resolvedConfig: ResolvedConfig;
4050
configPath?: string;
@@ -44,7 +54,7 @@ export class Config {
4454
_alias?: string;
4555

4656
plugins: Plugin[];
47-
ignore: ResolvedIgnore = {};
57+
ignore: Record<string, Record<string, Set<string>>> = {};
4858
doNotResolveExamples: boolean;
4959
rules: Record<SpecVersion, Record<string, RuleConfig>>;
5060
preprocessors: Record<SpecVersion, Record<string, PreprocessorConfig>>;
@@ -61,8 +71,6 @@ export class Config {
6171
resolvedRefMap?: ResolvedRefMap;
6272
alias?: string;
6373
plugins?: Plugin[];
64-
ignoreFile?: IgnoreFile;
65-
ignore?: ResolvedIgnore;
6674
} = {}
6775
) {
6876
this.resolvedConfig = resolvedConfig;
@@ -145,25 +153,7 @@ export class Config {
145153
},
146154
};
147155

148-
this.ignore = opts.ignore ?? (opts.ignoreFile ? this.resolveIgnore(opts.ignoreFile) : {});
149-
}
150-
151-
private resolveIgnore({ content, dir }: IgnoreFile): ResolvedIgnore {
152-
const ignore: ResolvedIgnore = Object.create(null);
153-
154-
for (const fileName of Object.keys(content)) {
155-
const fileIgnore = content[fileName];
156-
157-
const resolvedFileName = isAbsoluteUrl(fileName) ? fileName : resolvePath(dir, fileName);
158-
159-
ignore[resolvedFileName] = Object.create(null);
160-
161-
for (const ruleId of Object.keys(fileIgnore)) {
162-
ignore[resolvedFileName][ruleId] = new Set(fileIgnore[ruleId]);
163-
}
164-
}
165-
166-
return ignore;
156+
this.resolveIgnore(getIgnoreFilePath(opts.configPath));
167157
}
168158

169159
forAlias(alias?: string) {
@@ -181,11 +171,35 @@ export class Config {
181171
resolvedRefMap: this.resolvedRefMap,
182172
alias,
183173
plugins: this.plugins,
184-
ignore: this.ignore,
185174
}
186175
);
187176
}
188177

178+
resolveIgnore(ignoreFile?: string) {
179+
if (!ignoreFile || !doesYamlFileExist(ignoreFile)) return;
180+
181+
this.ignore =
182+
(parseYaml(fs.readFileSync(ignoreFile, 'utf-8')) as Record<
183+
string,
184+
Record<string, Set<string>>
185+
>) || {};
186+
187+
// resolve ignore paths
188+
for (const fileName of Object.keys(this.ignore)) {
189+
this.ignore[
190+
isAbsoluteUrl(fileName) ? fileName : path.resolve(path.dirname(ignoreFile), fileName)
191+
] = this.ignore[fileName];
192+
193+
for (const ruleId of Object.keys(this.ignore[fileName])) {
194+
this.ignore[fileName][ruleId] = new Set(this.ignore[fileName][ruleId]);
195+
}
196+
197+
if (!isAbsoluteUrl(fileName)) {
198+
delete this.ignore[fileName];
199+
}
200+
}
201+
}
202+
189203
saveIgnore() {
190204
const dir = this.configPath ? path.dirname(this.configPath) : process.cwd();
191205
const ignoreFile = path.join(dir, IGNORE_FILE);

0 commit comments

Comments
 (0)