Skip to content

Commit 9ce88a3

Browse files
authored
feat: add info-license-strict rule (#1653)
1 parent 013e91f commit 9ce88a3

File tree

14 files changed

+242
-6
lines changed

14 files changed

+242
-6
lines changed

.changeset/shy-crews-fix.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@redocly/openapi-core": minor
3+
"@redocly/cli": minor
4+
---
5+
6+
Added `info-license-strict` rule as a replacement of the `info-license-url` to support the OpenAPI 3.1 changes to allow identifier or URL license details.

packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
1313
"async2Rules": {
1414
"channels-kebab-case": "off",
1515
"info-contact": "off",
16+
"info-license-strict": "warn",
1617
"no-channel-trailing-slash": "off",
1718
"operation-operationId": "warn",
1819
"spec": "error",
@@ -24,6 +25,7 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
2425
"async3Rules": {
2526
"channels-kebab-case": "off",
2627
"info-contact": "off",
28+
"info-license-strict": "warn",
2729
"no-channel-trailing-slash": "off",
2830
"operation-operationId": "warn",
2931
"spec": "error",
@@ -86,7 +88,8 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
8688
"boolean-parameter-prefixes": "error",
8789
"info-contact": "off",
8890
"info-license": "warn",
89-
"info-license-url": "warn",
91+
"info-license-strict": "warn",
92+
"info-license-url": "off",
9093
"local/operation-id-not-test": "error",
9194
"no-ambiguous-paths": "warn",
9295
"no-enum-type-mismatch": "error",
@@ -142,6 +145,7 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
142145
"async2Rules": {
143146
"channels-kebab-case": "off",
144147
"info-contact": "off",
148+
"info-license-strict": "warn",
145149
"no-channel-trailing-slash": "off",
146150
"operation-operationId": "warn",
147151
"spec": "error",
@@ -153,6 +157,7 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
153157
"async3Rules": {
154158
"channels-kebab-case": "off",
155159
"info-contact": "off",
160+
"info-license-strict": "warn",
156161
"no-channel-trailing-slash": "off",
157162
"operation-operationId": "warn",
158163
"spec": "error",
@@ -233,7 +238,8 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
233238
"boolean-parameter-prefixes": "error",
234239
"info-contact": "off",
235240
"info-license": "warn",
236-
"info-license-url": "warn",
241+
"info-license-strict": "warn",
242+
"info-license-url": "off",
237243
"local/operation-id-not-test": "error",
238244
"no-ambiguous-paths": "warn",
239245
"no-enum-type-mismatch": "error",

packages/core/src/config/all.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
55
'info-contact': 'error',
66
'info-license': 'error',
77
'info-license-url': 'error',
8+
'info-license-strict': 'error',
89
'tag-description': 'error',
910
'tags-alphabetical': 'error',
1011
'parameter-description': 'error',
@@ -108,6 +109,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
108109
async2Rules: {
109110
spec: 'error',
110111
'info-contact': 'error',
112+
'info-license-strict': 'error',
111113
'operation-operationId': 'error',
112114
'tag-description': 'error',
113115
'tags-alphabetical': 'error',
@@ -117,6 +119,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
117119
async3Rules: {
118120
spec: 'error',
119121
'info-contact': 'error',
122+
'info-license-strict': 'error',
120123
'operation-operationId': 'error',
121124
'tag-description': 'error',
122125
'tags-alphabetical': 'error',

packages/core/src/config/minimal.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
55
'info-contact': 'off',
66
'info-license': 'off',
77
'info-license-url': 'off',
8+
'info-license-strict': 'off',
89
'tag-description': 'warn',
910
'tags-alphabetical': 'off',
1011
'parameter-description': 'off',
@@ -90,6 +91,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
9091
async2Rules: {
9192
spec: 'error',
9293
'info-contact': 'off',
94+
'info-license-strict': 'off',
9395
'operation-operationId': 'warn',
9496
'tag-description': 'warn',
9597
'tags-alphabetical': 'off',
@@ -99,6 +101,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
99101
async3Rules: {
100102
spec: 'error',
101103
'info-contact': 'off',
104+
'info-license-strict': 'off',
102105
'operation-operationId': 'warn',
103106
'tag-description': 'warn',
104107
'tags-alphabetical': 'off',

packages/core/src/config/recommended-strict.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
44
rules: {
55
'info-contact': 'off',
66
'info-license': 'error',
7-
'info-license-url': 'error',
7+
'info-license-url': 'off',
8+
'info-license-strict': 'error',
89
'tag-description': 'error',
910
'tags-alphabetical': 'off',
1011
'parameter-description': 'off',
@@ -90,6 +91,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
9091
async2Rules: {
9192
spec: 'error',
9293
'info-contact': 'off',
94+
'info-license-strict': 'error',
9395
'operation-operationId': 'error',
9496
'tag-description': 'error',
9597
'tags-alphabetical': 'off',
@@ -99,6 +101,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
99101
async3Rules: {
100102
spec: 'error',
101103
'info-contact': 'off',
104+
'info-license-strict': 'error',
102105
'operation-operationId': 'error',
103106
'tag-description': 'error',
104107
'tags-alphabetical': 'off',

packages/core/src/config/recommended.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
44
rules: {
55
'info-contact': 'off',
66
'info-license': 'warn',
7-
'info-license-url': 'warn',
7+
'info-license-url': 'off',
8+
'info-license-strict': 'warn',
89
'tag-description': 'warn',
910
'tags-alphabetical': 'off',
1011
'parameter-description': 'off',
@@ -90,6 +91,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
9091
async2Rules: {
9192
spec: 'error',
9293
'info-contact': 'off',
94+
'info-license-strict': 'warn',
9395
'operation-operationId': 'warn',
9496
'tag-description': 'warn',
9597
'tags-alphabetical': 'off',
@@ -99,6 +101,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
99101
async3Rules: {
100102
spec: 'error',
101103
'info-contact': 'off',
104+
'info-license-strict': 'warn',
102105
'operation-operationId': 'warn',
103106
'tag-description': 'warn',
104107
'tags-alphabetical': 'off',

packages/core/src/rules/async2/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Assertions } from '../common/assertions';
22
import { Spec } from '../common/spec';
33
import { InfoContact } from '../common/info-contact';
4+
import { InfoLicenseStrict } from '../common/info-license-strict';
45
import { OperationOperationId } from '../common/operation-operationId';
56
import { TagDescription } from '../common/tag-description';
67
import { TagsAlphabetical } from '../common/tags-alphabetical';
@@ -14,6 +15,7 @@ export const rules: Async2RuleSet<'built-in'> = {
1415
spec: Spec as Async2Rule,
1516
assertions: Assertions as Async2Rule,
1617
'info-contact': InfoContact as Async2Rule,
18+
'info-license-strict': InfoLicenseStrict as Async2Rule,
1719
'operation-operationId': OperationOperationId as Async2Rule,
1820
'channels-kebab-case': ChannelsKebabCase,
1921
'no-channel-trailing-slash': NoChannelTrailingSlash,

packages/core/src/rules/async3/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Assertions } from '../common/assertions';
22
import { Spec } from '../common/spec';
33
import { InfoContact } from '../common/info-contact';
4+
import { InfoLicenseStrict } from '../common/info-license-strict';
45
import { OperationOperationId } from '../common/operation-operationId';
56
import { TagDescription } from '../common/tag-description';
67
import { TagsAlphabetical } from '../common/tags-alphabetical';
@@ -14,6 +15,7 @@ export const rules: Async3RuleSet<'built-in'> = {
1415
spec: Spec as Async3Rule,
1516
assertions: Assertions as Async3Rule,
1617
'info-contact': InfoContact as Async3Rule,
18+
'info-license-strict': InfoLicenseStrict as Async3Rule,
1719
'operation-operationId': OperationOperationId as Async3Rule,
1820
'channels-kebab-case': ChannelsKebabCase,
1921
'no-channel-trailing-slash': NoChannelTrailingSlash,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { outdent } from 'outdent';
2+
import { lintDocument } from '../../../lint';
3+
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4+
import { BaseResolver } from '../../../resolve';
5+
6+
describe('license-strict', () => {
7+
it('should report on info.license with no url or identifier for OpenAPI 3.1', async () => {
8+
const document = parseYamlToDocument(
9+
outdent`
10+
openapi: 3.1.0
11+
info:
12+
license:
13+
name: MIT
14+
`,
15+
'foobar.yaml'
16+
);
17+
18+
const results = await lintDocument({
19+
externalRefResolver: new BaseResolver(),
20+
document,
21+
config: await makeConfig({ rules: { 'info-license-strict': 'error' } }),
22+
});
23+
24+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
25+
[
26+
{
27+
"location": [
28+
{
29+
"pointer": "#/info/license",
30+
"reportOnKey": true,
31+
"source": "foobar.yaml",
32+
},
33+
],
34+
"message": "License object should contain one of the fields: \`url\`, \`identifier\`.",
35+
"ruleId": "info-license-strict",
36+
"severity": "error",
37+
"suggest": [],
38+
},
39+
]
40+
`);
41+
});
42+
43+
it('should not report on info.license with url for OpenAPI 3.1', async () => {
44+
const document = parseYamlToDocument(
45+
outdent`
46+
openapi: 3.1.0
47+
info:
48+
license:
49+
name: MIT
50+
url: google.com
51+
`,
52+
'foobar.yaml'
53+
);
54+
55+
const results = await lintDocument({
56+
externalRefResolver: new BaseResolver(),
57+
document,
58+
config: await makeConfig({ rules: { 'info-license-strict': 'error' } }),
59+
});
60+
61+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
62+
});
63+
64+
it('should not report on info.license with identifier', async () => {
65+
const document = parseYamlToDocument(
66+
outdent`
67+
openapi: 3.1.0
68+
info:
69+
license:
70+
name: MIT
71+
identifier: MIT
72+
`,
73+
'foobar.yaml'
74+
);
75+
76+
const results = await lintDocument({
77+
externalRefResolver: new BaseResolver(),
78+
document,
79+
config: await makeConfig({ rules: { 'info-license-strict': 'error' } }),
80+
});
81+
82+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
83+
});
84+
85+
it('should report on info.license with no url for AsyncAPI 3.0', async () => {
86+
const document = parseYamlToDocument(
87+
outdent`
88+
asyncapi: 3.0.0
89+
info:
90+
license:
91+
name: MIT
92+
`,
93+
'foobar.yaml'
94+
);
95+
96+
const results = await lintDocument({
97+
externalRefResolver: new BaseResolver(),
98+
document,
99+
config: await makeConfig({ rules: { 'info-license-strict': 'error' } }),
100+
});
101+
102+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
103+
[
104+
{
105+
"location": [
106+
{
107+
"pointer": "#/info/license/url",
108+
"reportOnKey": true,
109+
"source": "foobar.yaml",
110+
},
111+
],
112+
"message": "License object should contain \`url\` field.",
113+
"ruleId": "info-license-strict",
114+
"severity": "error",
115+
"suggest": [],
116+
},
117+
]
118+
`);
119+
});
120+
121+
it('should not report on info.license with url for AsyncAPI 3.0', async () => {
122+
const document = parseYamlToDocument(
123+
outdent`
124+
asyncapi: 3.0.0
125+
info:
126+
license:
127+
name: MIT
128+
url: google.com
129+
`,
130+
'foobar.yaml'
131+
);
132+
133+
const results = await lintDocument({
134+
externalRefResolver: new BaseResolver(),
135+
document,
136+
config: await makeConfig({ rules: { 'info-license-strict': 'error' } }),
137+
});
138+
139+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
140+
});
141+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { detectSpec } from '../../oas-types';
2+
import { validateDefinedAndNonEmpty, validateOneOfDefinedAndNonEmpty } from '../utils';
3+
4+
import type { Oas3Rule, Oas2Rule, Async2Rule, Async3Rule } from '../../visitors';
5+
6+
export const InfoLicenseStrict: Oas2Rule | Oas3Rule | Async2Rule | Async3Rule = () => {
7+
let specVersion: string | undefined;
8+
return {
9+
Root: {
10+
enter(root: any) {
11+
specVersion = detectSpec(root);
12+
},
13+
License: {
14+
leave(license, ctx) {
15+
if (specVersion === 'oas3_1') {
16+
validateOneOfDefinedAndNonEmpty(['url', 'identifier'], license, ctx);
17+
} else {
18+
validateDefinedAndNonEmpty('url', license, ctx);
19+
}
20+
},
21+
},
22+
},
23+
};
24+
};

packages/core/src/rules/oas2/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examp
44
import { InfoContact } from '../common/info-contact';
55
import { InfoLicense } from '../common/info-license';
66
import { InfoLicenseUrl } from '../common/info-license-url';
7+
import { InfoLicenseStrict } from '../common/info-license-strict';
78
import { BooleanParameterPrefixes } from './boolean-parameter-prefixes';
89
import { TagDescription } from '../common/tag-description';
910
import { TagsAlphabetical } from '../common/tags-alphabetical';
@@ -52,6 +53,7 @@ export const rules: Oas2RuleSet<'built-in'> = {
5253
'info-contact': InfoContact as Oas2Rule,
5354
'info-license': InfoLicense as Oas2Rule,
5455
'info-license-url': InfoLicenseUrl as Oas2Rule,
56+
'info-license-strict': InfoLicenseStrict as Oas2Rule,
5557
'tag-description': TagDescription as Oas2Rule,
5658
'tags-alphabetical': TagsAlphabetical as Oas2Rule,
5759
'paths-kebab-case': PathsKebabCase as Oas2Rule,

packages/core/src/rules/oas3/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TagDescription } from '../common/tag-description';
1818
import { InfoContact } from '../common/info-contact';
1919
import { InfoLicense } from '../common/info-license';
2020
import { InfoLicenseUrl } from '../common/info-license-url';
21+
import { InfoLicenseStrict } from '../common/info-license-strict';
2122
import { OperationDescription } from '../common/operation-description';
2223
import { NoUnusedComponents } from './no-unused-components';
2324
import { PathNotIncludeQuery } from '../common/path-not-include-query';
@@ -62,6 +63,7 @@ export const rules: Oas3RuleSet<'built-in'> = {
6263
'info-contact': InfoContact as Oas3Rule,
6364
'info-license': InfoLicense as Oas3Rule,
6465
'info-license-url': InfoLicenseUrl as Oas3Rule,
66+
'info-license-strict': InfoLicenseStrict as Oas3Rule,
6567
'operation-2xx-response': Operation2xxResponse as Oas3Rule,
6668
'operation-4xx-response': Operation4xxResponse as Oas3Rule,
6769
'operation-4xx-problem-details-rfc7807': Operation4xxProblemDetailsRfc7807,

0 commit comments

Comments
 (0)