@redocly/openapi-core 1.0.0-beta.74 → 1.0.0-beta.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bundle.d.ts +4 -0
- package/lib/bundle.js +14 -2
- package/lib/config/config.d.ts +2 -9
- package/lib/format/format.d.ts +1 -1
- package/lib/format/format.js +39 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/rules/builtin.d.ts +2 -0
- package/lib/rules/common/remove-x-internal.d.ts +2 -0
- package/lib/rules/common/remove-x-internal.js +40 -0
- package/lib/rules/oas2/index.d.ts +1 -0
- package/lib/rules/oas2/index.js +2 -0
- package/lib/rules/oas2/remove-unused-components.d.ts +2 -0
- package/lib/rules/oas2/remove-unused-components.js +71 -0
- package/lib/rules/oas3/index.d.ts +1 -0
- package/lib/rules/oas3/index.js +2 -0
- package/lib/rules/oas3/remove-unused-components.d.ts +2 -0
- package/lib/rules/oas3/remove-unused-components.js +81 -0
- package/lib/typings/swagger.d.ts +14 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +9 -1
- package/lib/walk.js +4 -7
- package/package.json +1 -1
- package/src/bundle.ts +17 -1
- package/src/config/config.ts +2 -3
- package/src/format/format.ts +47 -2
- package/src/index.ts +1 -1
- package/src/rules/__tests__/config.ts +5 -4
- package/src/rules/__tests__/hide-internals.test.ts +234 -0
- package/src/rules/common/remove-x-internal.ts +42 -0
- package/src/rules/oas2/index.ts +3 -1
- package/src/rules/oas2/remove-unused-components.ts +74 -0
- package/src/rules/oas3/index.ts +2 -0
- package/src/rules/oas3/remove-unused-components.ts +82 -0
- package/src/types/oas2.ts +0 -3
- package/src/typings/swagger.ts +7 -0
- package/src/utils.ts +8 -0
- package/src/walk.ts +4 -9
- package/tsconfig.tsbuildinfo +1 -1
package/src/format/format.ts
CHANGED
|
@@ -46,7 +46,7 @@ function severityToNumber(severity: ProblemSeverity) {
|
|
|
46
46
|
return severity === 'error' ? 1 : 2;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export type OutputFormat = 'codeframe' | 'stylish' | 'json';
|
|
49
|
+
export type OutputFormat = 'codeframe' | 'stylish' | 'json' | 'checkstyle';
|
|
50
50
|
|
|
51
51
|
export function getTotals(problems: (NormalizedProblem & { ignored?: boolean })[]): Totals {
|
|
52
52
|
let errors = 0;
|
|
@@ -111,7 +111,7 @@ export function formatProblems(
|
|
|
111
111
|
process.stderr.write(`${formatCodeframe(problem, i)}\n`);
|
|
112
112
|
}
|
|
113
113
|
break;
|
|
114
|
-
case 'stylish':
|
|
114
|
+
case 'stylish': {
|
|
115
115
|
const groupedByFile = groupByFiles(problems);
|
|
116
116
|
for (const [file, { ruleIdPad, locationPad: positionPad, fileProblems }] of Object.entries(
|
|
117
117
|
groupedByFile,
|
|
@@ -126,6 +126,22 @@ export function formatProblems(
|
|
|
126
126
|
process.stderr.write('\n');
|
|
127
127
|
}
|
|
128
128
|
break;
|
|
129
|
+
}
|
|
130
|
+
case 'checkstyle': {
|
|
131
|
+
const groupedByFile = groupByFiles(problems);
|
|
132
|
+
|
|
133
|
+
process.stdout.write('<?xml version="1.0" encoding="UTF-8"?>\n');
|
|
134
|
+
process.stdout.write('<checkstyle version="4.3">\n');
|
|
135
|
+
|
|
136
|
+
for (const [file, { fileProblems }] of Object.entries(groupedByFile)) {
|
|
137
|
+
process.stdout.write(`<file name="${xmlEscape(path.relative(cwd, file))}">\n`);
|
|
138
|
+
fileProblems.forEach(formatCheckstyle);
|
|
139
|
+
process.stdout.write(`</file>\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.stdout.write(`</checkstyle>\n`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
if (totalProblems - ignoredProblems > maxProblems) {
|
|
@@ -204,6 +220,16 @@ export function formatProblems(
|
|
|
204
220
|
locationPad,
|
|
205
221
|
)} ${severityName} ${problem.ruleId.padEnd(ruleIdPad)} ${problem.message}`;
|
|
206
222
|
}
|
|
223
|
+
|
|
224
|
+
function formatCheckstyle(problem: OnlyLineColProblem) {
|
|
225
|
+
const { line, col } = problem.location[0].start;
|
|
226
|
+
const severity = problem.severity == 'warn' ? 'warning' : 'error';
|
|
227
|
+
const message = xmlEscape(problem.message);
|
|
228
|
+
const source = xmlEscape(problem.ruleId);
|
|
229
|
+
process.stdout.write(
|
|
230
|
+
`<error line="${line}" column="${col}" severity="${severity}" message="${message}" source="${source}" />\n`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
function formatFrom(cwd: string, location?: LocationObject) {
|
|
@@ -261,3 +287,22 @@ const groupByFiles = (problems: NormalizedProblem[]) => {
|
|
|
261
287
|
|
|
262
288
|
return fileGroups;
|
|
263
289
|
};
|
|
290
|
+
|
|
291
|
+
function xmlEscape(s: string): string {
|
|
292
|
+
return s.replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, (char) => {
|
|
293
|
+
switch (char) {
|
|
294
|
+
case '<':
|
|
295
|
+
return '<';
|
|
296
|
+
case '>':
|
|
297
|
+
return '>';
|
|
298
|
+
case '&':
|
|
299
|
+
return '&';
|
|
300
|
+
case '"':
|
|
301
|
+
return '"';
|
|
302
|
+
case "'":
|
|
303
|
+
return ''';
|
|
304
|
+
default:
|
|
305
|
+
return `&#${char.charCodeAt(0)};`;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -47,4 +47,4 @@ export {
|
|
|
47
47
|
export { getAstNodeByPointer, getLineColLocation } from './format/codeframes';
|
|
48
48
|
export { formatProblems, OutputFormat, getTotals, Totals } from './format/format';
|
|
49
49
|
export { lint, lint as validate, lintDocument, lintFromString, lintConfig } from './lint';
|
|
50
|
-
export { bundle, bundleDocument } from './bundle';
|
|
50
|
+
export { bundle, bundleDocument, mapTypeToComponent } from './bundle';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { LintConfig, RuleConfig } from
|
|
1
|
+
import { DecoratorConfig, LintConfig, RuleConfig } from '../../config/config';
|
|
2
2
|
import { defaultPlugin } from '../../config/builtIn';
|
|
3
3
|
|
|
4
|
-
export function makeConfig(rules: Record<string, RuleConfig>) {
|
|
4
|
+
export function makeConfig(rules: Record<string, RuleConfig>, decorators?: Record<string, DecoratorConfig>) {
|
|
5
5
|
return new LintConfig({
|
|
6
6
|
plugins: [defaultPlugin],
|
|
7
7
|
extends: [],
|
|
8
|
-
rules
|
|
8
|
+
rules,
|
|
9
|
+
decorators,
|
|
9
10
|
});
|
|
10
|
-
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { outdent } from 'outdent';
|
|
2
|
+
import { bundleDocument } from '../../bundle'
|
|
3
|
+
import { BaseResolver } from '../../resolve';
|
|
4
|
+
import { parseYamlToDocument, yamlSerializer } from '../../../__tests__/utils';
|
|
5
|
+
import { makeConfig } from './config';
|
|
6
|
+
|
|
7
|
+
describe('oas3 remove-x-internal', () => {
|
|
8
|
+
expect.addSnapshotSerializer(yamlSerializer);
|
|
9
|
+
const testDocument = parseYamlToDocument(
|
|
10
|
+
outdent`
|
|
11
|
+
openapi: 3.0.0
|
|
12
|
+
paths:
|
|
13
|
+
/pet:
|
|
14
|
+
removeit: true
|
|
15
|
+
get:
|
|
16
|
+
parameters:
|
|
17
|
+
- $ref: '#/components/parameters/x'
|
|
18
|
+
components:
|
|
19
|
+
parameters:
|
|
20
|
+
x:
|
|
21
|
+
name: x
|
|
22
|
+
`);
|
|
23
|
+
|
|
24
|
+
it('should use `internalFlagProperty` option to remove internal paths', async () => {
|
|
25
|
+
const { bundle: res } = await bundleDocument({
|
|
26
|
+
document: testDocument,
|
|
27
|
+
externalRefResolver: new BaseResolver(),
|
|
28
|
+
config: makeConfig({}, { 'remove-x-internal': { 'internalFlagProperty': 'removeit' } })
|
|
29
|
+
});
|
|
30
|
+
expect(res.parsed).toMatchInlineSnapshot(
|
|
31
|
+
`
|
|
32
|
+
openapi: 3.0.0
|
|
33
|
+
components:
|
|
34
|
+
parameters:
|
|
35
|
+
x:
|
|
36
|
+
name: x
|
|
37
|
+
|
|
38
|
+
`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should clean types: Server, Operation, Parameter, PathItem, Example', async () => {
|
|
42
|
+
const testDoc = parseYamlToDocument(
|
|
43
|
+
outdent`
|
|
44
|
+
openapi: 3.1.0
|
|
45
|
+
servers:
|
|
46
|
+
- url: //petstore.swagger.io/v2
|
|
47
|
+
description: Default server
|
|
48
|
+
x-internal: true
|
|
49
|
+
paths:
|
|
50
|
+
/pet:
|
|
51
|
+
get:
|
|
52
|
+
x-internal: true
|
|
53
|
+
operationId: getPet
|
|
54
|
+
parameters:
|
|
55
|
+
- $ref: '#/components/parameters/x'
|
|
56
|
+
put:
|
|
57
|
+
parameters:
|
|
58
|
+
- name: Accept-Language
|
|
59
|
+
x-internal: true
|
|
60
|
+
in: header
|
|
61
|
+
example: en-US
|
|
62
|
+
required: false
|
|
63
|
+
- name: cookieParam
|
|
64
|
+
x-internal: true
|
|
65
|
+
in: cookie
|
|
66
|
+
description: Some cookie
|
|
67
|
+
required: true
|
|
68
|
+
/admin:
|
|
69
|
+
x-internal: true
|
|
70
|
+
post:
|
|
71
|
+
parameters:
|
|
72
|
+
- $ref: '#/components/parameters/y'
|
|
73
|
+
/store/order:
|
|
74
|
+
post:
|
|
75
|
+
operationId: placeOrder
|
|
76
|
+
responses:
|
|
77
|
+
'200':
|
|
78
|
+
description: successful operation
|
|
79
|
+
content:
|
|
80
|
+
application/json:
|
|
81
|
+
examples:
|
|
82
|
+
response:
|
|
83
|
+
x-internal: true
|
|
84
|
+
value: OK
|
|
85
|
+
components:
|
|
86
|
+
parameters:
|
|
87
|
+
x:
|
|
88
|
+
name: x
|
|
89
|
+
y:
|
|
90
|
+
name: y
|
|
91
|
+
`);
|
|
92
|
+
const { bundle: res } = await bundleDocument({
|
|
93
|
+
document: testDoc,
|
|
94
|
+
externalRefResolver: new BaseResolver(),
|
|
95
|
+
config: makeConfig({}, { 'remove-x-internal': 'on' })
|
|
96
|
+
});
|
|
97
|
+
expect(res.parsed).toMatchInlineSnapshot(
|
|
98
|
+
`
|
|
99
|
+
openapi: 3.1.0
|
|
100
|
+
paths:
|
|
101
|
+
/pet:
|
|
102
|
+
put: {}
|
|
103
|
+
/store/order:
|
|
104
|
+
post:
|
|
105
|
+
operationId: placeOrder
|
|
106
|
+
responses:
|
|
107
|
+
'200':
|
|
108
|
+
description: successful operation
|
|
109
|
+
content:
|
|
110
|
+
application/json: {}
|
|
111
|
+
components:
|
|
112
|
+
parameters:
|
|
113
|
+
x:
|
|
114
|
+
name: x
|
|
115
|
+
'y':
|
|
116
|
+
name: 'y'
|
|
117
|
+
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should clean types: Schema, Response, RequestBody, MediaType, Callback', async () => {
|
|
123
|
+
const testDoc = parseYamlToDocument(
|
|
124
|
+
outdent`
|
|
125
|
+
openapi: 3.1.0
|
|
126
|
+
paths:
|
|
127
|
+
/pet:
|
|
128
|
+
post:
|
|
129
|
+
summary: test
|
|
130
|
+
requestBody:
|
|
131
|
+
x-internal: true
|
|
132
|
+
content:
|
|
133
|
+
application/x-www-form-urlencoded:
|
|
134
|
+
schema:
|
|
135
|
+
type: object
|
|
136
|
+
/store/order:
|
|
137
|
+
post:
|
|
138
|
+
operationId: storeOrder
|
|
139
|
+
parameters:
|
|
140
|
+
- name: api_key
|
|
141
|
+
schema:
|
|
142
|
+
x-internal: true
|
|
143
|
+
type: string
|
|
144
|
+
responses:
|
|
145
|
+
'200':
|
|
146
|
+
x-internal: true
|
|
147
|
+
content:
|
|
148
|
+
application/json:
|
|
149
|
+
examples:
|
|
150
|
+
response:
|
|
151
|
+
value: OK
|
|
152
|
+
requestBody:
|
|
153
|
+
content:
|
|
154
|
+
application/x-www-form-urlencoded:
|
|
155
|
+
x-internal: true
|
|
156
|
+
schema:
|
|
157
|
+
type: object
|
|
158
|
+
callbacks:
|
|
159
|
+
orderInProgress:
|
|
160
|
+
x-internal: true
|
|
161
|
+
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
|
|
162
|
+
servers:
|
|
163
|
+
- url: //callback-url.path-level/v1
|
|
164
|
+
description: Path level server
|
|
165
|
+
`);
|
|
166
|
+
const { bundle: res } = await bundleDocument({
|
|
167
|
+
document: testDoc,
|
|
168
|
+
externalRefResolver: new BaseResolver(),
|
|
169
|
+
config: makeConfig({}, { 'remove-x-internal': 'on' })
|
|
170
|
+
});
|
|
171
|
+
expect(res.parsed).toMatchInlineSnapshot(
|
|
172
|
+
`
|
|
173
|
+
openapi: 3.1.0
|
|
174
|
+
paths:
|
|
175
|
+
/pet:
|
|
176
|
+
post:
|
|
177
|
+
summary: test
|
|
178
|
+
/store/order:
|
|
179
|
+
post:
|
|
180
|
+
operationId: storeOrder
|
|
181
|
+
parameters:
|
|
182
|
+
- name: api_key
|
|
183
|
+
requestBody: {}
|
|
184
|
+
components: {}
|
|
185
|
+
|
|
186
|
+
`);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('oas2 remove-x-internal', () => {
|
|
191
|
+
it('should clean types - base test', async () => {
|
|
192
|
+
const testDoc = parseYamlToDocument(
|
|
193
|
+
outdent`
|
|
194
|
+
swagger: '2.0'
|
|
195
|
+
host: api.instagram.com
|
|
196
|
+
paths:
|
|
197
|
+
'/geographies/{geo-id}/media/recent':
|
|
198
|
+
get:
|
|
199
|
+
parameters:
|
|
200
|
+
- description: The geography ID.
|
|
201
|
+
x-internal: true
|
|
202
|
+
in: path
|
|
203
|
+
name: geo-id
|
|
204
|
+
required: true
|
|
205
|
+
type: string
|
|
206
|
+
- description: Max number of media to return.
|
|
207
|
+
x-internal: true
|
|
208
|
+
format: int32
|
|
209
|
+
in: query
|
|
210
|
+
name: count
|
|
211
|
+
required: false
|
|
212
|
+
type: integer
|
|
213
|
+
responses:
|
|
214
|
+
'200':
|
|
215
|
+
x-internal: true
|
|
216
|
+
description: List of recent media entries.
|
|
217
|
+
`);
|
|
218
|
+
const { bundle: res } = await bundleDocument({
|
|
219
|
+
document: testDoc,
|
|
220
|
+
externalRefResolver: new BaseResolver(),
|
|
221
|
+
config: makeConfig({}, { 'remove-x-internal': 'on' })
|
|
222
|
+
});
|
|
223
|
+
expect(res.parsed).toMatchInlineSnapshot(
|
|
224
|
+
`
|
|
225
|
+
swagger: '2.0'
|
|
226
|
+
host: api.instagram.com
|
|
227
|
+
paths:
|
|
228
|
+
/geographies/{geo-id}/media/recent:
|
|
229
|
+
get: {}
|
|
230
|
+
|
|
231
|
+
`
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
2
|
+
import { isEmptyArray, isEmptyObject, isPlainObject } from '../../utils';
|
|
3
|
+
import { UserContext } from '../../walk';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_INTERNAL_PROPERTY_NAME = 'x-internal';
|
|
6
|
+
|
|
7
|
+
export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagProperty }) => {
|
|
8
|
+
const hiddenTag = internalFlagProperty || DEFAULT_INTERNAL_PROPERTY_NAME;
|
|
9
|
+
|
|
10
|
+
function removeInternal(node: any, ctx: UserContext) {
|
|
11
|
+
const { parent, key } = ctx;
|
|
12
|
+
let didDelete = false;
|
|
13
|
+
if (Array.isArray(node)) {
|
|
14
|
+
for (let i = 0; i < node.length; i++) {
|
|
15
|
+
if (node[i] && node[i][hiddenTag]) {
|
|
16
|
+
node.splice(i, 1);
|
|
17
|
+
didDelete = true;
|
|
18
|
+
i--;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} else if (isPlainObject(node)) {
|
|
22
|
+
for (const key of Object.keys(node)) {
|
|
23
|
+
if ((node as any)[key][hiddenTag]) {
|
|
24
|
+
delete (node as any)[key];
|
|
25
|
+
didDelete = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (didDelete && (isEmptyObject(node) || isEmptyArray(node))) {
|
|
31
|
+
delete parent[key];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
any: {
|
|
37
|
+
enter: (node, ctx) => {
|
|
38
|
+
removeInternal(node, ctx);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/rules/oas2/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Oas2Decorator, Oas2Rule } from '../../visitors';
|
|
1
2
|
import { OasSpec } from '../common/spec';
|
|
2
3
|
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
3
4
|
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
@@ -26,7 +27,6 @@ import { OperationSingularTag } from '../common/operation-singular-tag';
|
|
|
26
27
|
import { OperationSecurityDefined } from '../common/operation-security-defined';
|
|
27
28
|
import { NoUnresolvedRefs } from '../no-unresolved-refs';
|
|
28
29
|
import { PathHttpVerbsOrder } from '../common/path-http-verbs-order';
|
|
29
|
-
import { Oas2Decorator, Oas2Rule } from '../../visitors';
|
|
30
30
|
import { RegistryDependencies } from '../common/registry-dependencies';
|
|
31
31
|
import { NoIdenticalPaths } from '../common/no-identical-paths';
|
|
32
32
|
import { OperationOperationId } from '../common/operation-operationId';
|
|
@@ -40,6 +40,7 @@ import { PathSegmentPlural } from '../common/path-segment-plural';
|
|
|
40
40
|
import { OperationDescriptionOverride } from '../common/operation-description-override';
|
|
41
41
|
import { TagDescriptionOverride } from '../common/tag-description-override';
|
|
42
42
|
import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
43
|
+
import { RemoveXInternal } from '../common/remove-x-internal';
|
|
43
44
|
|
|
44
45
|
export const rules = {
|
|
45
46
|
spec: OasSpec as Oas2Rule,
|
|
@@ -88,4 +89,5 @@ export const decorators = {
|
|
|
88
89
|
'operation-description-override': OperationDescriptionOverride as Oas2Decorator,
|
|
89
90
|
'tag-description-override': TagDescriptionOverride as Oas2Decorator,
|
|
90
91
|
'info-description-override': InfoDescriptionOverride as Oas2Decorator,
|
|
92
|
+
'remove-x-internal': RemoveXInternal as Oas2Decorator
|
|
91
93
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Oas2Rule } from '../../visitors';
|
|
2
|
+
import { Location } from '../../ref-utils';
|
|
3
|
+
import { Oas2Components } from '../../typings/swagger';
|
|
4
|
+
import { isEmptyObject } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const RemoveUnusedComponents: Oas2Rule = () => {
|
|
7
|
+
let components = new Map<string, { used: boolean; componentType?: keyof Oas2Components; name: string }>();
|
|
8
|
+
|
|
9
|
+
function registerComponent(location: Location, componentType: keyof Oas2Components, name: string): void {
|
|
10
|
+
components.set(location.absolutePointer, {
|
|
11
|
+
used: components.get(location.absolutePointer)?.used || false,
|
|
12
|
+
componentType,
|
|
13
|
+
name,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ref(ref, { type, resolve, key }) {
|
|
19
|
+
if (
|
|
20
|
+
['Schema', 'Parameter', 'Response', 'SecurityScheme'].includes(type.name)
|
|
21
|
+
) {
|
|
22
|
+
const resolvedRef = resolve(ref);
|
|
23
|
+
if (!resolvedRef.location) return;
|
|
24
|
+
components.set(resolvedRef.location.absolutePointer, {
|
|
25
|
+
used: true,
|
|
26
|
+
name: key.toString(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
DefinitionRoot: {
|
|
31
|
+
leave(root, ctx) {
|
|
32
|
+
const data = ctx.getVisitorData() as { removedCount: number };
|
|
33
|
+
data.removedCount = 0;
|
|
34
|
+
|
|
35
|
+
let rootComponents = new Set<keyof Oas2Components>();
|
|
36
|
+
components.forEach(usageInfo => {
|
|
37
|
+
const { used, name, componentType } = usageInfo;
|
|
38
|
+
if (!used && componentType) {
|
|
39
|
+
rootComponents.add(componentType);
|
|
40
|
+
delete root[componentType]![name];
|
|
41
|
+
data.removedCount++;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
for (const component of rootComponents) {
|
|
45
|
+
if (isEmptyObject(root[component])) {
|
|
46
|
+
delete root[component];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
NamedSchemas: {
|
|
52
|
+
Schema(schema, { location, key }) {
|
|
53
|
+
if (!schema.allOf) {
|
|
54
|
+
registerComponent(location, 'definitions', key.toString());
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
NamedParameters: {
|
|
59
|
+
Parameter(_parameter, { location, key }) {
|
|
60
|
+
registerComponent(location, 'parameters', key.toString());
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
NamedResponses: {
|
|
64
|
+
Response(_response, { location, key }) {
|
|
65
|
+
registerComponent(location, 'responses', key.toString());
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
NamedSecuritySchemes: {
|
|
69
|
+
SecurityScheme(_securityScheme, { location, key }) {
|
|
70
|
+
registerComponent(location, 'securityDefinitions', key.toString());
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
};
|
package/src/rules/oas3/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
|
49
49
|
import { PathExcludesPatterns } from '../common/path-excludes-patterns';
|
|
50
50
|
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
51
51
|
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
52
|
+
import { RemoveXInternal } from '../common/remove-x-internal';
|
|
52
53
|
|
|
53
54
|
export const rules = {
|
|
54
55
|
spec: OasSpec,
|
|
@@ -106,4 +107,5 @@ export const decorators = {
|
|
|
106
107
|
'operation-description-override': OperationDescriptionOverride as Oas3Decorator,
|
|
107
108
|
'tag-description-override': TagDescriptionOverride as Oas3Decorator,
|
|
108
109
|
'info-description-override': InfoDescriptionOverride as Oas3Decorator,
|
|
110
|
+
'remove-x-internal': RemoveXInternal as Oas3Decorator
|
|
109
111
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Oas3Rule } from '../../visitors';
|
|
2
|
+
import { Location } from '../../ref-utils';
|
|
3
|
+
import { Oas3Components } from '../../typings/openapi'
|
|
4
|
+
import { isEmptyObject } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const RemoveUnusedComponents: Oas3Rule = () => {
|
|
7
|
+
let components = new Map<string, { used: boolean; componentType?: keyof Oas3Components; name: string }>();
|
|
8
|
+
|
|
9
|
+
function registerComponent(location: Location, componentType: keyof Oas3Components, name: string): void {
|
|
10
|
+
components.set(location.absolutePointer, {
|
|
11
|
+
used: components.get(location.absolutePointer)?.used || false,
|
|
12
|
+
componentType,
|
|
13
|
+
name,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ref(ref, { type, resolve, key }) {
|
|
19
|
+
if (
|
|
20
|
+
['Schema', 'Header', 'Parameter', 'Response', 'Example', 'RequestBody'].includes(type.name)
|
|
21
|
+
) {
|
|
22
|
+
const resolvedRef = resolve(ref);
|
|
23
|
+
if (!resolvedRef.location) return;
|
|
24
|
+
components.set(resolvedRef.location.absolutePointer, {
|
|
25
|
+
used: true,
|
|
26
|
+
name: key.toString(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
DefinitionRoot: {
|
|
31
|
+
leave(root, ctx) {
|
|
32
|
+
const data = ctx.getVisitorData() as { removedCount: number };
|
|
33
|
+
data.removedCount = 0;
|
|
34
|
+
|
|
35
|
+
components.forEach(usageInfo => {
|
|
36
|
+
const { used, componentType, name } = usageInfo;
|
|
37
|
+
if (!used && componentType) {
|
|
38
|
+
let componentChild = root.components![componentType];
|
|
39
|
+
delete componentChild![name];
|
|
40
|
+
data.removedCount++;
|
|
41
|
+
if (isEmptyObject(componentChild)) {
|
|
42
|
+
delete root.components![componentType];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (isEmptyObject(root.components)) { delete root.components; }
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
NamedSchemas: {
|
|
50
|
+
Schema(schema, { location, key }) {
|
|
51
|
+
if (!schema.allOf) {
|
|
52
|
+
registerComponent(location, 'schemas', key.toString());
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
NamedParameters: {
|
|
57
|
+
Parameter(_parameter, { location, key }) {
|
|
58
|
+
registerComponent(location, 'parameters', key.toString());
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
NamedResponses: {
|
|
62
|
+
Response(_response, { location, key }) {
|
|
63
|
+
registerComponent(location, 'responses', key.toString());
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
NamedExamples: {
|
|
67
|
+
Example(_example, { location, key }) {
|
|
68
|
+
registerComponent(location, 'examples', key.toString());
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
NamedRequestBodies: {
|
|
72
|
+
RequestBody(_requestBody, { location, key }) {
|
|
73
|
+
registerComponent(location, 'requestBodies', key.toString());
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
NamedHeaders: {
|
|
77
|
+
Header(_header, { location, key }) {
|
|
78
|
+
registerComponent(location, 'headers', key.toString());
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
package/src/types/oas2.ts
CHANGED
|
@@ -383,13 +383,10 @@ export const Oas2Types: Record<string, NodeType> = {
|
|
|
383
383
|
Schema,
|
|
384
384
|
Xml,
|
|
385
385
|
SchemaProperties,
|
|
386
|
-
|
|
387
386
|
NamedSchemas: mapOf('Schema'),
|
|
388
387
|
NamedResponses: mapOf('Response'),
|
|
389
388
|
NamedParameters: mapOf('Parameter'),
|
|
390
389
|
NamedSecuritySchemes: mapOf('SecurityScheme'),
|
|
391
|
-
|
|
392
390
|
SecurityScheme,
|
|
393
|
-
|
|
394
391
|
XCodeSample,
|
|
395
392
|
};
|
package/src/typings/swagger.ts
CHANGED
|
@@ -20,6 +20,13 @@ export interface Oas2Definition {
|
|
|
20
20
|
externalDocs?: Oas2ExternalDocs;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface Oas2Components {
|
|
24
|
+
definitions?: { [name: string]: Record<string, Oas2Schema> };
|
|
25
|
+
securityDefinitions?: { [name: string]: Record<string, Oas2SecurityScheme> };
|
|
26
|
+
responses?: { [name: string]: Record<string, Oas2Response> };
|
|
27
|
+
parameters?: { [name: string]: Record<string, Oas2Parameter> };
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export interface Oas2Info {
|
|
24
31
|
title: string;
|
|
25
32
|
version: string;
|
package/src/utils.ts
CHANGED
|
@@ -40,6 +40,14 @@ export function isPlainObject(value: any): value is object {
|
|
|
40
40
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function isEmptyObject(value: any): value is object {
|
|
44
|
+
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isEmptyArray(value: any) {
|
|
48
|
+
return Array.isArray(value) && value.length === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export async function readFileFromUrl(url: string, config: HttpResolveConfig) {
|
|
44
52
|
const headers: Record<string, string> = {};
|
|
45
53
|
for (const header of config.headers) {
|
package/src/walk.ts
CHANGED
|
@@ -212,16 +212,14 @@ export function walkDocument<T>(opts: {
|
|
|
212
212
|
if (!activatedOn.skipped) {
|
|
213
213
|
visitedBySome = true;
|
|
214
214
|
enteredContexts.add(context);
|
|
215
|
-
const ignoreNextVisitorsOnNode = visitWithContext(
|
|
215
|
+
const { ignoreNextVisitorsOnNode } = visitWithContext(
|
|
216
216
|
visit,
|
|
217
217
|
resolvedNode,
|
|
218
218
|
context,
|
|
219
219
|
ruleId,
|
|
220
220
|
severity,
|
|
221
221
|
);
|
|
222
|
-
if (ignoreNextVisitorsOnNode)
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
222
|
+
if (ignoreNextVisitorsOnNode) break;
|
|
225
223
|
}
|
|
226
224
|
}
|
|
227
225
|
}
|
|
@@ -358,16 +356,13 @@ export function walkDocument<T>(opts: {
|
|
|
358
356
|
key,
|
|
359
357
|
parentLocations: collectParentsLocations(context),
|
|
360
358
|
oasVersion: ctx.oasVersion,
|
|
361
|
-
ignoreNextVisitorsOnNode: () => {
|
|
362
|
-
ignoreNextVisitorsOnNode = true;
|
|
363
|
-
},
|
|
359
|
+
ignoreNextVisitorsOnNode: () => { ignoreNextVisitorsOnNode = true; },
|
|
364
360
|
getVisitorData: getVisitorDataFn.bind(undefined, ruleId),
|
|
365
361
|
},
|
|
366
362
|
collectParents(context),
|
|
367
363
|
context,
|
|
368
364
|
);
|
|
369
|
-
|
|
370
|
-
return ignoreNextVisitorsOnNode;
|
|
365
|
+
return { ignoreNextVisitorsOnNode };
|
|
371
366
|
}
|
|
372
367
|
|
|
373
368
|
function resolve<T>(
|