@redocly/openapi-core 1.10.6 → 1.12.0
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/CHANGELOG.md +17 -0
- package/lib/config/config-resolvers.d.ts +5 -1
- package/lib/config/config-resolvers.js +4 -4
- package/lib/config/load.d.ts +1 -0
- package/lib/config/load.js +7 -2
- package/lib/config/utils.js +1 -1
- package/lib/decorators/oas2/remove-unused-components.js +36 -22
- package/lib/decorators/oas3/remove-unused-components.js +34 -19
- package/lib/format/format.d.ts +1 -1
- package/lib/format/format.js +57 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +3 -2
- package/lib/rules/common/assertions/utils.js +2 -2
- package/lib/rules/oas3/no-invalid-media-type-examples.js +3 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +7 -1
- package/package.json +2 -2
- package/src/__tests__/format.test.ts +34 -0
- package/src/bundle.ts +1 -1
- package/src/config/__tests__/config-resolvers.test.ts +4 -4
- package/src/config/__tests__/fixtures/load-external.yaml +2 -0
- package/src/config/__tests__/load.test.ts +14 -0
- package/src/config/config-resolvers.ts +13 -7
- package/src/config/load.ts +13 -4
- package/src/config/utils.ts +1 -1
- package/src/decorators/oas2/__tests__/remove-unused-components.test.ts +74 -1
- package/src/decorators/oas2/remove-unused-components.ts +46 -24
- package/src/decorators/oas3/__tests__/remove-unused-components.test.ts +142 -0
- package/src/decorators/oas3/remove-unused-components.ts +45 -20
- package/src/format/format.ts +62 -1
- package/src/index.ts +8 -1
- package/src/rules/common/assertions/utils.ts +2 -2
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +145 -0
- package/src/rules/oas3/no-invalid-media-type-examples.ts +3 -0
- package/src/utils.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/load.ts
CHANGED
|
@@ -20,6 +20,7 @@ async function addConfigMetadata({
|
|
|
20
20
|
tokens,
|
|
21
21
|
files,
|
|
22
22
|
region,
|
|
23
|
+
externalRefResolver,
|
|
23
24
|
}: {
|
|
24
25
|
rawConfig: RawConfig;
|
|
25
26
|
customExtends?: string[];
|
|
@@ -27,6 +28,7 @@ async function addConfigMetadata({
|
|
|
27
28
|
tokens?: RegionalTokenWithValidity[];
|
|
28
29
|
files?: string[];
|
|
29
30
|
region?: Region;
|
|
31
|
+
externalRefResolver?: BaseResolver;
|
|
30
32
|
}): Promise<Config> {
|
|
31
33
|
if (customExtends !== undefined) {
|
|
32
34
|
rawConfig.styleguide = rawConfig.styleguide || {};
|
|
@@ -64,10 +66,15 @@ async function addConfigMetadata({
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
return resolveConfig(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
return resolveConfig({
|
|
70
|
+
rawConfig: {
|
|
71
|
+
...rawConfig,
|
|
72
|
+
files: files ?? rawConfig.files,
|
|
73
|
+
region: region ?? rawConfig.region,
|
|
74
|
+
},
|
|
75
|
+
configPath,
|
|
76
|
+
externalRefResolver,
|
|
77
|
+
});
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export type RawConfigProcessor = (
|
|
@@ -105,6 +112,7 @@ export async function loadConfig(
|
|
|
105
112
|
tokens,
|
|
106
113
|
files,
|
|
107
114
|
region,
|
|
115
|
+
externalRefResolver,
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -156,6 +164,7 @@ type CreateConfigOptions = {
|
|
|
156
164
|
extends?: string[];
|
|
157
165
|
tokens?: RegionalTokenWithValidity[];
|
|
158
166
|
configPath?: string;
|
|
167
|
+
externalRefResolver?: BaseResolver;
|
|
159
168
|
};
|
|
160
169
|
|
|
161
170
|
export async function createConfig(
|
package/src/config/utils.ts
CHANGED
|
@@ -173,7 +173,7 @@ export function mergeExtends(rulesConfList: ResolvedStyleguideConfig[]) {
|
|
|
173
173
|
for (const rulesConf of rulesConfList) {
|
|
174
174
|
if (rulesConf.extends) {
|
|
175
175
|
throw new Error(
|
|
176
|
-
`'extends' is not supported in shared configs yet
|
|
176
|
+
`'extends' is not supported in shared configs yet:\n${JSON.stringify(rulesConf, null, 2)}`
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { outdent } from 'outdent';
|
|
2
|
-
import { parseYamlToDocument,
|
|
2
|
+
import { parseYamlToDocument, makeConfig } from '../../../../__tests__/utils';
|
|
3
3
|
import { bundleDocument } from '../../../bundle';
|
|
4
4
|
import { BaseResolver } from '../../../resolve';
|
|
5
5
|
|
|
@@ -152,4 +152,77 @@ describe('oas2 remove-unused-components', () => {
|
|
|
152
152
|
},
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
it('should remove transitively unused components', async () => {
|
|
157
|
+
const document = parseYamlToDocument(
|
|
158
|
+
outdent`
|
|
159
|
+
swagger: '2.0'
|
|
160
|
+
paths:
|
|
161
|
+
/pets:
|
|
162
|
+
get:
|
|
163
|
+
produces:
|
|
164
|
+
- application/json
|
|
165
|
+
parameters: []
|
|
166
|
+
responses:
|
|
167
|
+
'200':
|
|
168
|
+
schema:
|
|
169
|
+
$ref: '#/definitions/Used'
|
|
170
|
+
operationId: listPets
|
|
171
|
+
summary: List all pets
|
|
172
|
+
definitions:
|
|
173
|
+
Unused:
|
|
174
|
+
enum:
|
|
175
|
+
- 1
|
|
176
|
+
- 2
|
|
177
|
+
type: integer
|
|
178
|
+
UnusedTransitive:
|
|
179
|
+
type: object
|
|
180
|
+
properties:
|
|
181
|
+
link:
|
|
182
|
+
$ref: '#/definitions/Unused'
|
|
183
|
+
Used:
|
|
184
|
+
properties:
|
|
185
|
+
link:
|
|
186
|
+
type: string
|
|
187
|
+
type: object
|
|
188
|
+
`,
|
|
189
|
+
'foobar.yaml'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const results = await bundleDocument({
|
|
193
|
+
externalRefResolver: new BaseResolver(),
|
|
194
|
+
document,
|
|
195
|
+
config: await makeConfig({}),
|
|
196
|
+
removeUnusedComponents: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(results.bundle.parsed).toEqual({
|
|
200
|
+
swagger: '2.0',
|
|
201
|
+
definitions: {
|
|
202
|
+
Used: {
|
|
203
|
+
properties: {
|
|
204
|
+
link: { type: 'string' },
|
|
205
|
+
},
|
|
206
|
+
type: 'object',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
paths: {
|
|
210
|
+
'/pets': {
|
|
211
|
+
get: {
|
|
212
|
+
produces: ['application/json'],
|
|
213
|
+
parameters: [],
|
|
214
|
+
summary: 'List all pets',
|
|
215
|
+
operationId: 'listPets',
|
|
216
|
+
responses: {
|
|
217
|
+
'200': {
|
|
218
|
+
schema: {
|
|
219
|
+
$ref: '#/definitions/Used',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
});
|
|
155
228
|
});
|
|
@@ -2,12 +2,12 @@ import { Location } from '../../ref-utils';
|
|
|
2
2
|
import { isEmptyObject } from '../../utils';
|
|
3
3
|
|
|
4
4
|
import type { Oas2Decorator } from '../../visitors';
|
|
5
|
-
import type { Oas2Components } from '../../typings/swagger';
|
|
5
|
+
import type { Oas2Components, Oas2Definition } from '../../typings/swagger';
|
|
6
6
|
|
|
7
7
|
export const RemoveUnusedComponents: Oas2Decorator = () => {
|
|
8
8
|
const components = new Map<
|
|
9
9
|
string,
|
|
10
|
-
{
|
|
10
|
+
{ usedIn: Location[]; componentType?: keyof Oas2Components; name: string }
|
|
11
11
|
>();
|
|
12
12
|
|
|
13
13
|
function registerComponent(
|
|
@@ -16,15 +16,46 @@ export const RemoveUnusedComponents: Oas2Decorator = () => {
|
|
|
16
16
|
name: string
|
|
17
17
|
): void {
|
|
18
18
|
components.set(location.absolutePointer, {
|
|
19
|
-
|
|
19
|
+
usedIn: components.get(location.absolutePointer)?.usedIn ?? [],
|
|
20
20
|
componentType,
|
|
21
21
|
name,
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function removeUnusedComponents(root: Oas2Definition, removedPaths: string[]): number {
|
|
26
|
+
const removedLengthStart = removedPaths.length;
|
|
27
|
+
|
|
28
|
+
for (const [path, { usedIn, name, componentType }] of components) {
|
|
29
|
+
const used = usedIn.some(
|
|
30
|
+
(location) =>
|
|
31
|
+
!removedPaths.some(
|
|
32
|
+
(removed) =>
|
|
33
|
+
// Check if the current location's absolute pointer starts with the 'removed' path
|
|
34
|
+
// and either its length matches exactly with 'removed' or the character after the 'removed' path is a '/'
|
|
35
|
+
location.absolutePointer.startsWith(removed) &&
|
|
36
|
+
(location.absolutePointer.length === removed.length ||
|
|
37
|
+
location.absolutePointer[removed.length] === '/')
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
if (!used && componentType) {
|
|
41
|
+
removedPaths.push(path);
|
|
42
|
+
delete root[componentType]![name];
|
|
43
|
+
components.delete(path);
|
|
44
|
+
|
|
45
|
+
if (isEmptyObject(root[componentType])) {
|
|
46
|
+
delete root[componentType];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return removedPaths.length > removedLengthStart
|
|
52
|
+
? removeUnusedComponents(root, removedPaths)
|
|
53
|
+
: removedPaths.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
return {
|
|
26
57
|
ref: {
|
|
27
|
-
leave(ref, { type, resolve, key }) {
|
|
58
|
+
leave(ref, { location, type, resolve, key }) {
|
|
28
59
|
if (['Schema', 'Parameter', 'Response', 'SecurityScheme'].includes(type.name)) {
|
|
29
60
|
const resolvedRef = resolve(ref);
|
|
30
61
|
if (!resolvedRef.location) return;
|
|
@@ -33,32 +64,23 @@ export const RemoveUnusedComponents: Oas2Decorator = () => {
|
|
|
33
64
|
const componentLevelLocalPointer = localPointer.split('/').slice(0, 3).join('/');
|
|
34
65
|
const pointer = `${fileLocation}#${componentLevelLocalPointer}`;
|
|
35
66
|
|
|
36
|
-
components.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
const registered = components.get(pointer);
|
|
68
|
+
|
|
69
|
+
if (registered) {
|
|
70
|
+
registered.usedIn.push(location);
|
|
71
|
+
} else {
|
|
72
|
+
components.set(pointer, {
|
|
73
|
+
usedIn: [location],
|
|
74
|
+
name: key.toString(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
40
77
|
}
|
|
41
78
|
},
|
|
42
79
|
},
|
|
43
80
|
Root: {
|
|
44
81
|
leave(root, ctx) {
|
|
45
82
|
const data = ctx.getVisitorData() as { removedCount: number };
|
|
46
|
-
data.removedCount =
|
|
47
|
-
|
|
48
|
-
const rootComponents = new Set<keyof Oas2Components>();
|
|
49
|
-
components.forEach((usageInfo) => {
|
|
50
|
-
const { used, name, componentType } = usageInfo;
|
|
51
|
-
if (!used && componentType) {
|
|
52
|
-
rootComponents.add(componentType);
|
|
53
|
-
delete root[componentType]![name];
|
|
54
|
-
data.removedCount++;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
for (const component of rootComponents) {
|
|
58
|
-
if (isEmptyObject(root[component])) {
|
|
59
|
-
delete root[component];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
83
|
+
data.removedCount = removeUnusedComponents(root, []);
|
|
62
84
|
},
|
|
63
85
|
},
|
|
64
86
|
NamedSchemas: {
|
|
@@ -168,4 +168,146 @@ describe('oas3 remove-unused-components', () => {
|
|
|
168
168
|
},
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
it('should remove transitively unused components', async () => {
|
|
173
|
+
const document = parseYamlToDocument(
|
|
174
|
+
outdent`
|
|
175
|
+
openapi: "3.0.0"
|
|
176
|
+
paths:
|
|
177
|
+
/pets:
|
|
178
|
+
get:
|
|
179
|
+
summary: List all pets
|
|
180
|
+
operationId: listPets
|
|
181
|
+
parameters:
|
|
182
|
+
- $ref: '#/components/parameters/used'
|
|
183
|
+
components:
|
|
184
|
+
parameters:
|
|
185
|
+
used:
|
|
186
|
+
name: used
|
|
187
|
+
unused:
|
|
188
|
+
name: unused
|
|
189
|
+
schemas:
|
|
190
|
+
Unused:
|
|
191
|
+
type: integer
|
|
192
|
+
enum:
|
|
193
|
+
- 1
|
|
194
|
+
- 2
|
|
195
|
+
Transitive:
|
|
196
|
+
type: object
|
|
197
|
+
properties:
|
|
198
|
+
link:
|
|
199
|
+
$ref: '#/components/schemas/Unused'
|
|
200
|
+
`,
|
|
201
|
+
'foobar.yaml'
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const results = await bundleDocument({
|
|
205
|
+
externalRefResolver: new BaseResolver(),
|
|
206
|
+
document,
|
|
207
|
+
config: await makeConfig({}),
|
|
208
|
+
removeUnusedComponents: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(results.bundle.parsed).toEqual({
|
|
212
|
+
openapi: '3.0.0',
|
|
213
|
+
paths: {
|
|
214
|
+
'/pets': {
|
|
215
|
+
get: {
|
|
216
|
+
summary: 'List all pets',
|
|
217
|
+
operationId: 'listPets',
|
|
218
|
+
parameters: [
|
|
219
|
+
{
|
|
220
|
+
$ref: '#/components/parameters/used',
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
components: {
|
|
227
|
+
parameters: {
|
|
228
|
+
used: {
|
|
229
|
+
name: 'used',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should remove transitively unused components with colliding paths', async () => {
|
|
237
|
+
const document = parseYamlToDocument(
|
|
238
|
+
outdent`
|
|
239
|
+
openapi: "3.0.0"
|
|
240
|
+
paths:
|
|
241
|
+
/pets:
|
|
242
|
+
get:
|
|
243
|
+
responses:
|
|
244
|
+
200:
|
|
245
|
+
content:
|
|
246
|
+
application/json:
|
|
247
|
+
schema:
|
|
248
|
+
$ref: "#/components/schemas/Transitive2"
|
|
249
|
+
components:
|
|
250
|
+
schemas:
|
|
251
|
+
Unused: # <-- this will be removed correctly
|
|
252
|
+
type: integer
|
|
253
|
+
Transitive: # <-- this will be removed correctly
|
|
254
|
+
type: object
|
|
255
|
+
properties:
|
|
256
|
+
link:
|
|
257
|
+
$ref: '#/components/schemas/Unused'
|
|
258
|
+
Used:
|
|
259
|
+
type: integer
|
|
260
|
+
Transitive2:
|
|
261
|
+
type: object
|
|
262
|
+
properties:
|
|
263
|
+
link:
|
|
264
|
+
$ref: '#/components/schemas/Used'
|
|
265
|
+
`,
|
|
266
|
+
|
|
267
|
+
'foobar.yaml'
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const results = await bundleDocument({
|
|
271
|
+
externalRefResolver: new BaseResolver(),
|
|
272
|
+
document,
|
|
273
|
+
config: await makeConfig({}),
|
|
274
|
+
removeUnusedComponents: true,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(results.bundle.parsed).toEqual({
|
|
278
|
+
openapi: '3.0.0',
|
|
279
|
+
paths: {
|
|
280
|
+
'/pets': {
|
|
281
|
+
get: {
|
|
282
|
+
responses: {
|
|
283
|
+
200: {
|
|
284
|
+
content: {
|
|
285
|
+
'application/json': {
|
|
286
|
+
schema: {
|
|
287
|
+
$ref: '#/components/schemas/Transitive2',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
components: {
|
|
297
|
+
schemas: {
|
|
298
|
+
Transitive2: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
link: {
|
|
302
|
+
$ref: '#/components/schemas/Used',
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
Used: {
|
|
307
|
+
type: 'integer',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
});
|
|
171
313
|
});
|
|
@@ -2,12 +2,12 @@ import { Location } from '../../ref-utils';
|
|
|
2
2
|
import { isEmptyObject } from '../../utils';
|
|
3
3
|
|
|
4
4
|
import type { Oas3Decorator } from '../../visitors';
|
|
5
|
-
import type { Oas3Components } from '../../typings/openapi';
|
|
5
|
+
import type { Oas3Components, Oas3Definition } from '../../typings/openapi';
|
|
6
6
|
|
|
7
7
|
export const RemoveUnusedComponents: Oas3Decorator = () => {
|
|
8
8
|
const components = new Map<
|
|
9
9
|
string,
|
|
10
|
-
{
|
|
10
|
+
{ usedIn: Location[]; componentType?: keyof Oas3Components; name: string }
|
|
11
11
|
>();
|
|
12
12
|
|
|
13
13
|
function registerComponent(
|
|
@@ -16,15 +16,45 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
|
|
|
16
16
|
name: string
|
|
17
17
|
): void {
|
|
18
18
|
components.set(location.absolutePointer, {
|
|
19
|
-
|
|
19
|
+
usedIn: components.get(location.absolutePointer)?.usedIn ?? [],
|
|
20
20
|
componentType,
|
|
21
21
|
name,
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function removeUnusedComponents(root: Oas3Definition, removedPaths: string[]): number {
|
|
26
|
+
const removedLengthStart = removedPaths.length;
|
|
27
|
+
|
|
28
|
+
for (const [path, { usedIn, name, componentType }] of components) {
|
|
29
|
+
const used = usedIn.some(
|
|
30
|
+
(location) =>
|
|
31
|
+
!removedPaths.some(
|
|
32
|
+
(removed) =>
|
|
33
|
+
location.absolutePointer.startsWith(removed) &&
|
|
34
|
+
(location.absolutePointer.length === removed.length ||
|
|
35
|
+
location.absolutePointer[removed.length] === '/')
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!used && componentType && root.components) {
|
|
40
|
+
removedPaths.push(path);
|
|
41
|
+
const componentChild = root.components[componentType];
|
|
42
|
+
delete componentChild![name];
|
|
43
|
+
components.delete(path);
|
|
44
|
+
if (isEmptyObject(componentChild)) {
|
|
45
|
+
delete root.components[componentType];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return removedPaths.length > removedLengthStart
|
|
51
|
+
? removeUnusedComponents(root, removedPaths)
|
|
52
|
+
: removedPaths.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
return {
|
|
26
56
|
ref: {
|
|
27
|
-
leave(ref, { type, resolve, key }) {
|
|
57
|
+
leave(ref, { location, type, resolve, key }) {
|
|
28
58
|
if (
|
|
29
59
|
['Schema', 'Header', 'Parameter', 'Response', 'Example', 'RequestBody'].includes(
|
|
30
60
|
type.name
|
|
@@ -37,29 +67,24 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
|
|
|
37
67
|
const componentLevelLocalPointer = localPointer.split('/').slice(0, 4).join('/');
|
|
38
68
|
const pointer = `${fileLocation}#${componentLevelLocalPointer}`;
|
|
39
69
|
|
|
40
|
-
components.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
const registered = components.get(pointer);
|
|
71
|
+
|
|
72
|
+
if (registered) {
|
|
73
|
+
registered.usedIn.push(location);
|
|
74
|
+
} else {
|
|
75
|
+
components.set(pointer, {
|
|
76
|
+
usedIn: [location],
|
|
77
|
+
name: key.toString(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
44
80
|
}
|
|
45
81
|
},
|
|
46
82
|
},
|
|
47
83
|
Root: {
|
|
48
84
|
leave(root, ctx) {
|
|
49
85
|
const data = ctx.getVisitorData() as { removedCount: number };
|
|
50
|
-
data.removedCount =
|
|
86
|
+
data.removedCount = removeUnusedComponents(root, []);
|
|
51
87
|
|
|
52
|
-
components.forEach((usageInfo) => {
|
|
53
|
-
const { used, componentType, name } = usageInfo;
|
|
54
|
-
if (!used && componentType && root.components) {
|
|
55
|
-
const componentChild = root.components[componentType];
|
|
56
|
-
delete componentChild![name];
|
|
57
|
-
data.removedCount++;
|
|
58
|
-
if (isEmptyObject(componentChild)) {
|
|
59
|
-
delete root.components[componentType];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
88
|
if (isEmptyObject(root.components)) {
|
|
64
89
|
delete root.components;
|
|
65
90
|
}
|
package/src/format/format.ts
CHANGED
|
@@ -51,7 +51,9 @@ export type OutputFormat =
|
|
|
51
51
|
| 'json'
|
|
52
52
|
| 'checkstyle'
|
|
53
53
|
| 'codeclimate'
|
|
54
|
-
| 'summary'
|
|
54
|
+
| 'summary'
|
|
55
|
+
| 'github-actions'
|
|
56
|
+
| 'markdown';
|
|
55
57
|
|
|
56
58
|
export function getTotals(problems: (NormalizedProblem & { ignored?: boolean })[]): Totals {
|
|
57
59
|
let errors = 0;
|
|
@@ -155,6 +157,8 @@ export function formatProblems(
|
|
|
155
157
|
case 'summary':
|
|
156
158
|
formatSummary(problems);
|
|
157
159
|
break;
|
|
160
|
+
case 'github-actions':
|
|
161
|
+
outputForGithubActions(problems, cwd);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
if (totalProblems - ignoredProblems > maxProblems) {
|
|
@@ -373,3 +377,60 @@ function xmlEscape(s: string): string {
|
|
|
373
377
|
}
|
|
374
378
|
});
|
|
375
379
|
}
|
|
380
|
+
|
|
381
|
+
function outputForGithubActions(problems: NormalizedProblem[], cwd: string): void {
|
|
382
|
+
for (const problem of problems) {
|
|
383
|
+
for (const location of problem.location.map(getLineColLocation)) {
|
|
384
|
+
let command;
|
|
385
|
+
switch (problem.severity) {
|
|
386
|
+
case 'error':
|
|
387
|
+
command = 'error';
|
|
388
|
+
break;
|
|
389
|
+
case 'warn':
|
|
390
|
+
command = 'warning';
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
const suggest = formatDidYouMean(problem);
|
|
394
|
+
const message = suggest !== '' ? problem.message + '\n\n' + suggest : problem.message;
|
|
395
|
+
const properties = {
|
|
396
|
+
title: problem.ruleId,
|
|
397
|
+
file: isAbsoluteUrl(location.source.absoluteRef)
|
|
398
|
+
? location.source.absoluteRef
|
|
399
|
+
: path.relative(cwd, location.source.absoluteRef),
|
|
400
|
+
line: location.start.line,
|
|
401
|
+
col: location.start.col,
|
|
402
|
+
endLine: location.end?.line,
|
|
403
|
+
endColumn: location.end?.col,
|
|
404
|
+
};
|
|
405
|
+
output.write(`::${command} ${formatProperties(properties)}::${escapeMessage(message)}\n`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatProperties(props: Record<string, any>): string {
|
|
410
|
+
return Object.entries(props)
|
|
411
|
+
.filter(([, v]) => v !== null && v !== undefined)
|
|
412
|
+
.map(([k, v]) => `${k}=${escapeProperty(v)}`)
|
|
413
|
+
.join(',');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function toString(v: any): string {
|
|
417
|
+
if (v === null || v === undefined) {
|
|
418
|
+
return '';
|
|
419
|
+
} else if (typeof v === 'string' || v instanceof String) {
|
|
420
|
+
return v as string;
|
|
421
|
+
}
|
|
422
|
+
return JSON.stringify(v);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function escapeMessage(v: any): string {
|
|
426
|
+
return toString(v).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
427
|
+
}
|
|
428
|
+
function escapeProperty(v: any): string {
|
|
429
|
+
return toString(v)
|
|
430
|
+
.replace(/%/g, '%25')
|
|
431
|
+
.replace(/\r/g, '%0D')
|
|
432
|
+
.replace(/\n/g, '%0A')
|
|
433
|
+
.replace(/:/g, '%3A')
|
|
434
|
+
.replace(/,/g, '%2C');
|
|
435
|
+
}
|
|
436
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
BundleOutputFormat,
|
|
3
|
+
readFileFromUrl,
|
|
4
|
+
slash,
|
|
5
|
+
doesYamlFileExist,
|
|
6
|
+
isTruthy,
|
|
7
|
+
pause,
|
|
8
|
+
} from './utils';
|
|
2
9
|
export { Oas3_1Types } from './types/oas3_1';
|
|
3
10
|
export { Oas3Types } from './types/oas3';
|
|
4
11
|
export { Oas2Types } from './types/oas2';
|
|
@@ -77,13 +77,13 @@ export function getAssertsToApply(assertion: AssertionDefinition): AssertToApply
|
|
|
77
77
|
|
|
78
78
|
if (shouldRunOnValues && !assertion.subject.property) {
|
|
79
79
|
throw new Error(
|
|
80
|
-
|
|
80
|
+
`The '${shouldRunOnValues.name}' assertion can't be used on all keys. Please provide a single property.`
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (shouldRunOnKeys && assertion.subject.property) {
|
|
85
85
|
throw new Error(
|
|
86
|
-
|
|
86
|
+
`The '${shouldRunOnKeys.name}' assertion can't be used on properties. Please remove the 'property' key.`
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|