@redocly/openapi-core 1.0.0-beta.111 → 1.0.0-beta.113

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.
Files changed (78) hide show
  1. package/lib/config/all.js +0 -1
  2. package/lib/config/config-resolvers.js +22 -18
  3. package/lib/config/config.d.ts +4 -10
  4. package/lib/config/config.js +1 -1
  5. package/lib/config/load.d.ts +1 -1
  6. package/lib/config/load.js +10 -10
  7. package/lib/config/minimal.js +0 -1
  8. package/lib/config/recommended.js +0 -1
  9. package/lib/config/rules.d.ts +6 -3
  10. package/lib/config/rules.js +3 -2
  11. package/lib/config/types.d.ts +3 -0
  12. package/lib/ref-utils.d.ts +1 -0
  13. package/lib/ref-utils.js +5 -1
  14. package/lib/resolve.js +19 -0
  15. package/lib/rules/common/assertions/asserts.d.ts +22 -5
  16. package/lib/rules/common/assertions/asserts.js +25 -0
  17. package/lib/rules/common/assertions/index.d.ts +27 -2
  18. package/lib/rules/common/assertions/index.js +6 -29
  19. package/lib/rules/common/assertions/utils.d.ts +7 -14
  20. package/lib/rules/common/assertions/utils.js +129 -97
  21. package/lib/rules/common/spec.js +6 -0
  22. package/lib/rules/oas2/index.d.ts +0 -1
  23. package/lib/rules/oas2/index.js +0 -2
  24. package/lib/rules/oas3/index.js +0 -2
  25. package/lib/rules/utils.js +3 -0
  26. package/lib/types/oas2.js +11 -7
  27. package/lib/types/oas3.js +15 -10
  28. package/lib/types/oas3_1.js +1 -0
  29. package/lib/types/redocly-yaml.js +49 -27
  30. package/lib/utils.d.ts +2 -0
  31. package/lib/utils.js +13 -1
  32. package/lib/visitors.d.ts +2 -1
  33. package/lib/visitors.js +1 -0
  34. package/lib/walk.js +7 -1
  35. package/package.json +1 -1
  36. package/src/__tests__/bundle.test.ts +46 -0
  37. package/src/__tests__/lint.test.ts +24 -5
  38. package/src/benchmark/benches/rebilly.yaml +36 -28
  39. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +1 -3
  40. package/src/config/__tests__/config-resolvers.test.ts +6 -7
  41. package/src/config/__tests__/fixtures/load-redocly.yaml +2 -0
  42. package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +6 -5
  43. package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +0 -1
  44. package/src/config/__tests__/load.test.ts +4 -1
  45. package/src/config/all.ts +0 -1
  46. package/src/config/config-resolvers.ts +44 -31
  47. package/src/config/config.ts +6 -5
  48. package/src/config/load.ts +19 -9
  49. package/src/config/minimal.ts +0 -1
  50. package/src/config/recommended.ts +0 -1
  51. package/src/config/rules.ts +11 -3
  52. package/src/config/types.ts +2 -0
  53. package/src/ref-utils.ts +4 -0
  54. package/src/resolve.ts +25 -3
  55. package/src/rules/common/__tests__/spec.test.ts +170 -0
  56. package/src/rules/common/assertions/__tests__/asserts.test.ts +7 -3
  57. package/src/rules/common/assertions/__tests__/index.test.ts +41 -20
  58. package/src/rules/common/assertions/__tests__/utils.test.ts +43 -17
  59. package/src/rules/common/assertions/asserts.ts +60 -8
  60. package/src/rules/common/assertions/index.ts +36 -46
  61. package/src/rules/common/assertions/utils.ts +204 -127
  62. package/src/rules/common/spec.ts +7 -0
  63. package/src/rules/oas2/index.ts +0 -2
  64. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +32 -0
  65. package/src/rules/oas3/index.ts +0 -2
  66. package/src/rules/utils.ts +4 -0
  67. package/src/types/oas2.ts +11 -7
  68. package/src/types/oas3.ts +15 -10
  69. package/src/types/oas3_1.ts +1 -0
  70. package/src/types/redocly-yaml.ts +49 -29
  71. package/src/utils.ts +11 -0
  72. package/src/visitors.ts +7 -1
  73. package/src/walk.ts +8 -1
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/lib/rules/common/info-description.d.ts +0 -2
  76. package/lib/rules/common/info-description.js +0 -12
  77. package/src/rules/common/__tests__/info-description.test.ts +0 -102
  78. package/src/rules/common/info-description.ts +0 -10
@@ -9,12 +9,33 @@ import {
9
9
  regexFromString,
10
10
  } from './utils';
11
11
 
12
- type Asserts = Record<
13
- string,
14
- (value: any, condition: any, baseLocation: Location, rawValue?: any) => AssertResult[]
15
- >;
12
+ export type AssertionFn = (
13
+ value: any,
14
+ condition: any,
15
+ baseLocation: Location,
16
+ rawValue?: any
17
+ ) => AssertResult[];
16
18
 
17
- export const runOnKeysSet = new Set([
19
+ export type Asserts = {
20
+ pattern: AssertionFn;
21
+ enum: AssertionFn;
22
+ defined: AssertionFn;
23
+ required: AssertionFn;
24
+ disallowed: AssertionFn;
25
+ undefined: AssertionFn;
26
+ nonEmpty: AssertionFn;
27
+ minLength: AssertionFn;
28
+ maxLength: AssertionFn;
29
+ casing: AssertionFn;
30
+ sortOrder: AssertionFn;
31
+ mutuallyExclusive: AssertionFn;
32
+ mutuallyRequired: AssertionFn;
33
+ requireAny: AssertionFn;
34
+ ref: AssertionFn;
35
+ const: AssertionFn;
36
+ };
37
+
38
+ export const runOnKeysSet = new Set<keyof Asserts>([
18
39
  'mutuallyExclusive',
19
40
  'mutuallyRequired',
20
41
  'enum',
@@ -27,8 +48,10 @@ export const runOnKeysSet = new Set([
27
48
  'required',
28
49
  'requireAny',
29
50
  'ref',
51
+ 'const',
52
+ 'defined', // In case if `property` for assertions is not added
30
53
  ]);
31
- export const runOnValuesSet = new Set([
54
+ export const runOnValuesSet = new Set<keyof Asserts>([
32
55
  'pattern',
33
56
  'enum',
34
57
  'defined',
@@ -39,6 +62,7 @@ export const runOnValuesSet = new Set([
39
62
  'casing',
40
63
  'sortOrder',
41
64
  'ref',
65
+ 'const',
42
66
  ]);
43
67
 
44
68
  export const asserts: Asserts = {
@@ -106,6 +130,34 @@ export const asserts: Asserts = {
106
130
  )
107
131
  .filter(isTruthy);
108
132
  },
133
+ const: (
134
+ value: string | number | boolean | string[] | number[],
135
+ condition: string | number | boolean,
136
+ baseLocation: Location
137
+ ) => {
138
+ if (typeof value === 'undefined') return [];
139
+
140
+ if (Array.isArray(value)) {
141
+ return value
142
+ .map(
143
+ (_val) =>
144
+ condition !== _val && {
145
+ message: `"${_val}" should be equal ${condition} `,
146
+ location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
147
+ }
148
+ )
149
+ .filter(isTruthy);
150
+ } else {
151
+ return value !== condition
152
+ ? [
153
+ {
154
+ message: `${value} should be equal ${condition}`,
155
+ location: baseLocation,
156
+ },
157
+ ]
158
+ : [];
159
+ }
160
+ },
109
161
  undefined: (value: any, condition: boolean = true, baseLocation: Location) => {
110
162
  const isUndefined = typeof value === 'undefined';
111
163
  const isValid = condition ? isUndefined : !isUndefined;
@@ -210,7 +262,7 @@ export const asserts: Asserts = {
210
262
  },
211
263
  ];
212
264
  },
213
- ref: (_value: any, condition: string | boolean, baseLocation, rawValue: any) => {
265
+ ref: (_value: any, condition: string | boolean, baseLocation: Location, rawValue: any) => {
214
266
  if (typeof rawValue === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
215
267
  const hasRef = rawValue.hasOwnProperty('$ref');
216
268
  if (typeof condition === 'boolean') {
@@ -237,7 +289,7 @@ export const asserts: Asserts = {
237
289
  },
238
290
  };
239
291
 
240
- export function buildAssertCustomFunction(fn: CustomFunction) {
292
+ export function buildAssertCustomFunction(fn: CustomFunction): AssertionFn {
241
293
  return (value: string[], options: any, baseLocation: Location) =>
242
294
  fn.call(null, value, options, baseLocation);
243
295
  }
@@ -1,65 +1,55 @@
1
- import { asserts, runOnKeysSet, runOnValuesSet } from './asserts';
2
- import { AssertToApply, buildSubjectVisitor, buildVisitorObject } from './utils';
3
- import { Oas2Rule, Oas3Rule } from '../../../visitors';
1
+ import { asserts, AssertionFn } from './asserts';
2
+ import { buildSubjectVisitor, buildVisitorObject } from './utils';
3
+ import { Oas2Visitor, Oas3Visitor } from '../../../visitors';
4
+ import { RuleSeverity } from '../../../config';
5
+ import { isString } from '../../../utils';
6
+
7
+ export type AssertionLocators = {
8
+ filterInParentKeys?: (string | number)[];
9
+ filterOutParentKeys?: (string | number)[];
10
+ matchParentKeys?: string;
11
+ };
12
+
13
+ export type AssertionDefinition = {
14
+ subject: {
15
+ type: string;
16
+ property?: string | string[];
17
+ } & AssertionLocators;
18
+ assertions: { [name in keyof typeof asserts]?: AssertionFn };
19
+ };
4
20
 
5
- export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
6
- const visitors: any[] = [];
21
+ export type RawAssertion = AssertionDefinition & {
22
+ where?: AssertionDefinition[];
23
+ message?: string;
24
+ suggest?: string[];
25
+ severity?: RuleSeverity;
26
+ };
27
+
28
+ export type Assertion = RawAssertion & { assertionId: string };
29
+
30
+ export const Assertions = (opts: Record<string, Assertion>) => {
31
+ const visitors: (Oas2Visitor | Oas3Visitor)[] = [];
7
32
 
8
33
  // As 'Assertions' has an array of asserts,
9
34
  // that array spreads into an 'opts' object on init rules phase here
10
35
  // https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/config/config.ts#L311
11
36
  // that is why we need to iterate through 'opts' values;
12
37
  // before - filter only object 'opts' values
13
- const assertions: any[] = Object.values(opts).filter(
38
+ const assertions: Assertion[] = Object.values(opts).filter(
14
39
  (opt: unknown) => typeof opt === 'object' && opt !== null
15
40
  );
16
41
 
17
42
  for (const [index, assertion] of assertions.entries()) {
18
43
  const assertId =
19
44
  (assertion.assertionId && `${assertion.assertionId} assertion`) || `assertion #${index + 1}`;
20
- if (!assertion.subject) {
21
- throw new Error(`${assertId}: 'subject' is required`);
22
- }
23
45
 
24
- const subjects: string[] = Array.isArray(assertion.subject)
25
- ? assertion.subject
26
- : [assertion.subject];
27
-
28
- const assertsToApply: AssertToApply[] = Object.keys(asserts)
29
- .filter((assertName: string) => assertion[assertName] !== undefined)
30
- .map((assertName: string) => {
31
- return {
32
- name: assertName,
33
- conditions: assertion[assertName],
34
- runsOnKeys: runOnKeysSet.has(assertName),
35
- runsOnValues: runOnValuesSet.has(assertName),
36
- };
37
- });
38
-
39
- const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
40
- (assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
41
- );
42
- const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
43
- (assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
44
- );
45
-
46
- if (shouldRunOnValues && !assertion.property) {
47
- throw new Error(
48
- `${shouldRunOnValues.name} can't be used on all keys. Please provide a single property.`
49
- );
46
+ if (!isString(assertion.subject.type)) {
47
+ throw new Error(`${assertId}: 'type' (String) is required`);
50
48
  }
51
49
 
52
- if (shouldRunOnKeys && assertion.property) {
53
- throw new Error(
54
- `${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`
55
- );
56
- }
57
-
58
- for (const subject of subjects) {
59
- const subjectVisitor = buildSubjectVisitor(assertId, assertion, assertsToApply);
60
- const visitorObject = buildVisitorObject(subject, assertion.context, subjectVisitor);
61
- visitors.push(visitorObject);
62
- }
50
+ const subjectVisitor = buildSubjectVisitor(assertId, assertion);
51
+ const visitorObject = buildVisitorObject(assertion, subjectVisitor);
52
+ visitors.push(visitorObject);
63
53
  }
64
54
 
65
55
  return visitors;
@@ -1,8 +1,15 @@
1
- import type { AssertResult, RuleSeverity } from '../../../config';
1
+ import { asserts, runOnKeysSet, runOnValuesSet, Asserts } from './asserts';
2
2
  import { colorize } from '../../../logger';
3
3
  import { isRef, Location } from '../../../ref-utils';
4
- import { UserContext } from '../../../walk';
5
- import { asserts } from './asserts';
4
+ import { isTruthy, keysOf, isString } from '../../../utils';
5
+ import type { AssertResult } from '../../../config';
6
+ import type { Assertion, AssertionDefinition, AssertionLocators } from '.';
7
+ import type {
8
+ Oas2Visitor,
9
+ Oas3Visitor,
10
+ SkipFunctionContext,
11
+ VisitFunction,
12
+ } from '../../../visitors';
6
13
 
7
14
  export type OrderDirection = 'asc' | 'desc';
8
15
 
@@ -11,166 +18,236 @@ export type OrderOptions = {
11
18
  property: string;
12
19
  };
13
20
 
14
- type Assertion = {
15
- property: string | string[];
16
- context?: Record<string, any>[];
17
- severity?: RuleSeverity;
18
- suggest?: any[];
19
- message?: string;
20
- subject: string;
21
- };
22
-
23
21
  export type AssertToApply = {
24
- name: string;
22
+ name: keyof Asserts;
25
23
  conditions: any;
26
24
  runsOnKeys: boolean;
27
25
  runsOnValues: boolean;
28
26
  };
29
27
 
28
+ type AssertionContext = SkipFunctionContext & {
29
+ node: any;
30
+ };
31
+
32
+ const assertionMessageTemplates = {
33
+ problems: '{{problems}}',
34
+ };
35
+
36
+ function getPredicatesFromLocators(
37
+ locators: AssertionLocators
38
+ ): ((key: string | number) => boolean)[] {
39
+ const { filterInParentKeys, filterOutParentKeys, matchParentKeys } = locators;
40
+
41
+ const keyMatcher = matchParentKeys && regexFromString(matchParentKeys);
42
+ const matchKeysPredicate =
43
+ keyMatcher && ((key: string | number) => keyMatcher.test(key.toString()));
44
+
45
+ const filterInPredicate =
46
+ Array.isArray(filterInParentKeys) &&
47
+ ((key: string | number) => filterInParentKeys.includes(key.toString()));
48
+
49
+ const filterOutPredicate =
50
+ Array.isArray(filterOutParentKeys) &&
51
+ ((key: string | number) => !filterOutParentKeys.includes(key.toString()));
52
+
53
+ return [matchKeysPredicate, filterInPredicate, filterOutPredicate].filter(isTruthy);
54
+ }
55
+
56
+ export function getAssertsToApply(assertion: AssertionDefinition): AssertToApply[] {
57
+ const assertsToApply = keysOf(asserts)
58
+ .filter((assertName) => assertion.assertions[assertName] !== undefined)
59
+ .map((assertName) => {
60
+ return {
61
+ name: assertName,
62
+ conditions: assertion.assertions[assertName],
63
+ runsOnKeys: runOnKeysSet.has(assertName),
64
+ runsOnValues: runOnValuesSet.has(assertName),
65
+ };
66
+ });
67
+
68
+ const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
69
+ (assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
70
+ );
71
+ const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
72
+ (assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
73
+ );
74
+
75
+ if (shouldRunOnValues && !assertion.subject.property) {
76
+ throw new Error(
77
+ `${shouldRunOnValues.name} can't be used on all keys. Please provide a single property`
78
+ );
79
+ }
80
+
81
+ if (shouldRunOnKeys && assertion.subject.property) {
82
+ throw new Error(
83
+ `${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`
84
+ );
85
+ }
86
+
87
+ return assertsToApply;
88
+ }
89
+
90
+ function getAssertionProperties({ subject }: AssertionDefinition): string[] {
91
+ return (Array.isArray(subject.property) ? subject.property : [subject?.property]).filter(
92
+ Boolean
93
+ ) as string[];
94
+ }
95
+
96
+ function applyAssertions(
97
+ assertionDefinition: AssertionDefinition,
98
+ asserts: AssertToApply[],
99
+ { rawLocation, rawNode, resolve, location, node }: AssertionContext
100
+ ): AssertResult[] {
101
+ const properties = getAssertionProperties(assertionDefinition);
102
+ const assertResults: Array<AssertResult[]> = [];
103
+
104
+ for (const assert of asserts) {
105
+ const currentLocation = assert.name === 'ref' ? rawLocation : location;
106
+
107
+ if (properties.length) {
108
+ for (const property of properties) {
109
+ // we can have resolvable scalar so need to resolve value here.
110
+ const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
111
+ assertResults.push(
112
+ runAssertion({
113
+ values: value,
114
+ rawValues: rawNode[property],
115
+ assert,
116
+ location: currentLocation.child(property),
117
+ })
118
+ );
119
+ }
120
+ } else {
121
+ const value = assert.name === 'ref' ? rawNode : Object.keys(node);
122
+ assertResults.push(
123
+ runAssertion({
124
+ values: Object.keys(node),
125
+ rawValues: value,
126
+ assert,
127
+ location: currentLocation,
128
+ })
129
+ );
130
+ }
131
+ }
132
+
133
+ return assertResults.flat();
134
+ }
135
+
30
136
  export function buildVisitorObject(
31
- subject: string,
32
- context: Record<string, any>[],
33
- subjectVisitor: any
34
- ) {
35
- if (!context) {
36
- return { [subject]: subjectVisitor };
137
+ assertion: Assertion,
138
+ subjectVisitor: VisitFunction<any>
139
+ ): Oas2Visitor | Oas3Visitor {
140
+ const targetVisitorLocatorPredicates = getPredicatesFromLocators(assertion.subject);
141
+ const targetVisitorSkipFunction = targetVisitorLocatorPredicates.length
142
+ ? (node: any, key: string | number) =>
143
+ !targetVisitorLocatorPredicates.every((predicate) => predicate(key))
144
+ : undefined;
145
+ const targetVisitor: Oas2Visitor | Oas3Visitor = {
146
+ [assertion.subject.type]: {
147
+ enter: subjectVisitor,
148
+ ...(targetVisitorSkipFunction && { skip: targetVisitorSkipFunction }),
149
+ },
150
+ };
151
+
152
+ if (!Array.isArray(assertion.where)) {
153
+ return targetVisitor;
37
154
  }
38
155
 
39
156
  let currentVisitorLevel: Record<string, any> = {};
40
157
  const visitor: Record<string, any> = currentVisitorLevel;
158
+ const context = assertion.where;
41
159
 
42
160
  for (let index = 0; index < context.length; index++) {
43
- const node = context[index];
44
- if (context.length === index + 1 && node.type === subject) {
45
- // Visitors don't work properly for the same type nested nodes, so
46
- // as a workaround for that we don't create separate visitor for the last element
47
- // which is the same as subject;
48
- // we will check includes/excludes it in the last visitor.
49
- continue;
50
- }
51
- const matchParentKeys = node.matchParentKeys;
52
- const excludeParentKeys = node.excludeParentKeys;
161
+ const assertionDefinitionNode = context[index];
53
162
 
54
- if (matchParentKeys && excludeParentKeys) {
163
+ if (!isString(assertionDefinitionNode.subject?.type)) {
55
164
  throw new Error(
56
- `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item`
165
+ `${assertion.assertionId} -> where -> [${index}]: 'type' (String) is required`
57
166
  );
58
167
  }
59
168
 
60
- if (matchParentKeys || excludeParentKeys) {
61
- currentVisitorLevel[node.type] = {
62
- skip: (_value: any, key: string) => {
63
- if (matchParentKeys) {
64
- return !matchParentKeys.includes(key);
65
- }
66
- if (excludeParentKeys) {
67
- return excludeParentKeys.includes(key);
68
- }
69
- },
169
+ const locatorPredicates = getPredicatesFromLocators(assertionDefinitionNode.subject);
170
+ const assertsToApply = getAssertsToApply(assertionDefinitionNode);
171
+
172
+ const skipFunction = (
173
+ node: unknown,
174
+ key: string | number,
175
+ { location, rawLocation, resolve, rawNode }: SkipFunctionContext
176
+ ): boolean =>
177
+ !locatorPredicates.every((predicate) => predicate(key)) ||
178
+ !!applyAssertions(assertionDefinitionNode, assertsToApply, {
179
+ location,
180
+ node,
181
+ rawLocation,
182
+ rawNode,
183
+ resolve,
184
+ }).length;
185
+
186
+ const nodeVisitor = {
187
+ ...((locatorPredicates.length || assertsToApply.length) && { skip: skipFunction }),
188
+ };
189
+
190
+ if (
191
+ assertionDefinitionNode.subject.type === assertion.subject.type &&
192
+ index === context.length - 1
193
+ ) {
194
+ // We have to merge the visitors if the last node inside the `where` is the same as the subject.
195
+ targetVisitor[assertion.subject.type] = {
196
+ enter: subjectVisitor,
197
+ ...((nodeVisitor.skip && { skip: nodeVisitor.skip }) ||
198
+ (targetVisitorSkipFunction && {
199
+ skip: (
200
+ node,
201
+ key,
202
+ ctx // We may have locators defined on assertion level and on where level for the same node type
203
+ ) => !!(nodeVisitor.skip?.(node, key, ctx) || targetVisitorSkipFunction?.(node, key)),
204
+ })),
70
205
  };
71
206
  } else {
72
- currentVisitorLevel[node.type] = {};
207
+ currentVisitorLevel = currentVisitorLevel[assertionDefinitionNode.subject?.type] =
208
+ nodeVisitor;
73
209
  }
74
- currentVisitorLevel = currentVisitorLevel[node.type];
75
210
  }
76
211
 
77
- currentVisitorLevel[subject] = subjectVisitor;
212
+ currentVisitorLevel[assertion.subject.type] = targetVisitor[assertion.subject.type];
78
213
 
79
214
  return visitor;
80
215
  }
81
216
 
82
- export function buildSubjectVisitor(
83
- assertId: string,
84
- assertion: Assertion,
85
- asserts: AssertToApply[]
86
- ) {
87
- return (
88
- node: any,
89
- { report, location, rawLocation, key, type, resolve, rawNode }: UserContext
90
- ) => {
91
- let properties = assertion.property;
92
- // We need to check context's last node if it has the same type as subject node;
93
- // if yes - that means we didn't create context's last node visitor,
94
- // so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
95
- if (assertion.context) {
96
- const lastContextNode = assertion.context[assertion.context.length - 1];
97
- if (lastContextNode.type === type.name) {
98
- const matchParentKeys = lastContextNode.matchParentKeys;
99
- const excludeParentKeys = lastContextNode.excludeParentKeys;
100
-
101
- if (matchParentKeys && !matchParentKeys.includes(key)) {
102
- return;
103
- }
104
- if (excludeParentKeys && excludeParentKeys.includes(key)) {
105
- return;
106
- }
107
- }
108
- }
109
-
110
- if (properties) {
111
- properties = Array.isArray(properties) ? properties : [properties];
112
- }
217
+ export function buildSubjectVisitor(assertId: string, assertion: Assertion): VisitFunction<any> {
218
+ return (node: any, { report, location, rawLocation, resolve, rawNode }) => {
219
+ const properties = getAssertionProperties(assertion);
113
220
 
114
221
  const defaultMessage = `${colorize.blue(assertId)} failed because the ${colorize.blue(
115
- assertion.subject
116
- )}${colorize.blue(
117
- properties ? ` ${(properties as string[]).join(', ')}` : ''
118
- )} didn't meet the assertions: {{problems}}`;
119
-
120
- const assertResults: Array<AssertResult[]> = [];
121
- for (const assert of asserts) {
122
- const currentLocation = assert.name === 'ref' ? rawLocation : location;
123
- if (properties) {
124
- for (const property of properties) {
125
- // we can have resolvable scalar so need to resolve value here.
126
- const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
127
- assertResults.push(
128
- runAssertion({
129
- values: value,
130
- rawValues: rawNode[property],
131
- assert,
132
- location: currentLocation.child(property),
133
- })
134
- );
135
- }
136
- } else {
137
- const value = assert.name === 'ref' ? rawNode : Object.keys(node);
138
- assertResults.push(
139
- runAssertion({
140
- values: Object.keys(node),
141
- rawValues: value,
142
- assert,
143
- location: currentLocation,
144
- })
145
- );
146
- }
147
- }
222
+ assertion.subject.type
223
+ )} ${colorize.blue(properties.join(', '))} didn't meet the assertions: ${
224
+ assertionMessageTemplates.problems
225
+ }`.replace(/ +/g, ' ');
226
+
227
+ const problems = applyAssertions(assertion, getAssertsToApply(assertion), {
228
+ rawLocation,
229
+ rawNode,
230
+ resolve,
231
+ location,
232
+ node,
233
+ });
148
234
 
149
- const problems = assertResults.flat();
150
235
  if (problems.length) {
151
236
  const message = assertion.message || defaultMessage;
152
-
153
- report({
154
- message: message.replace('{{problems}}', getProblemsMessage(problems)),
155
- location: getProblemsLocation(problems) || location,
156
- forceSeverity: assertion.severity || 'error',
157
- suggest: assertion.suggest || [],
158
- ruleId: assertId,
159
- });
237
+ for (const problem of problems) {
238
+ const problemMessage = problem.message ? problem.message : defaultMessage;
239
+ report({
240
+ message: message.replace(assertionMessageTemplates.problems, problemMessage),
241
+ location: problem.location || location,
242
+ forceSeverity: assertion.severity || 'error',
243
+ suggest: assertion.suggest || [],
244
+ ruleId: assertId,
245
+ });
246
+ }
160
247
  }
161
248
  };
162
249
  }
163
250
 
164
- function getProblemsLocation(problems: AssertResult[]) {
165
- return problems.length ? problems[0].location : undefined;
166
- }
167
-
168
- function getProblemsMessage(problems: AssertResult[]) {
169
- return problems.length === 1
170
- ? problems[0].message ?? ''
171
- : problems.map((problem) => `\n- ${problem.message ?? ''}`).join('');
172
- }
173
-
174
251
  export function getIntersectionLength(keys: string[], properties: string[]): number {
175
252
  const props = new Set(properties);
176
253
  let count = 0;
@@ -158,6 +158,13 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
158
158
  });
159
159
  }
160
160
  }
161
+
162
+ if (propName === 'nullable' && !node.type) {
163
+ report({
164
+ message: 'The `type` field must be defined when the `nullable` field is used.',
165
+ location: location.child([propName]),
166
+ });
167
+ }
161
168
  }
162
169
  },
163
170
  };
@@ -2,7 +2,6 @@ import { Oas2Rule } from '../../visitors';
2
2
  import { OasSpec } from '../common/spec';
3
3
  import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
4
4
  import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
5
- import { InfoDescription } from '../common/info-description';
6
5
  import { InfoContact } from '../common/info-contact';
7
6
  import { InfoLicense } from '../common/info-license';
8
7
  import { InfoLicenseUrl } from '../common/info-license-url';
@@ -45,7 +44,6 @@ export const rules = {
45
44
  spec: OasSpec as Oas2Rule,
46
45
  'no-invalid-schema-examples': NoInvalidSchemaExamples,
47
46
  'no-invalid-parameter-examples': NoInvalidParameterExamples,
48
- 'info-description': InfoDescription as Oas2Rule,
49
47
  'info-contact': InfoContact as Oas2Rule,
50
48
  'info-license': InfoLicense as Oas2Rule,
51
49
  'info-license-url': InfoLicenseUrl as Oas2Rule,
@@ -438,4 +438,36 @@ describe('no-invalid-media-type-examples', () => {
438
438
  ]
439
439
  `);
440
440
  });
441
+
442
+ it('should not report if allOf used with discriminator', async () => {
443
+ const document = parseYamlToDocument(
444
+ outdent`
445
+ openapi: 3.0.0
446
+ paths:
447
+ /pet:
448
+ get:
449
+ responses:
450
+ '200':
451
+ content:
452
+ application/json:
453
+ schema:
454
+ discriminator:
455
+ propertyName: powerSource
456
+ mapping: {}
457
+ allOf: []
458
+ examples:
459
+ first:
460
+ value: {}
461
+ `,
462
+ 'foobar.yaml'
463
+ );
464
+
465
+ const results = await lintDocument({
466
+ externalRefResolver: new BaseResolver(),
467
+ document,
468
+ config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
469
+ });
470
+
471
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
472
+ });
441
473
  });
@@ -15,7 +15,6 @@ import { OperationIdUrlSafe } from '../common/operation-operationId-url-safe';
15
15
  import { TagsAlphabetical } from '../common/tags-alphabetical';
16
16
  import { NoServerExample } from './no-server-example.com';
17
17
  import { NoServerTrailingSlash } from './no-server-trailing-slash';
18
- import { InfoDescription } from '../common/info-description';
19
18
  import { TagDescription } from '../common/tag-description';
20
19
  import { InfoContact } from '../common/info-contact';
21
20
  import { InfoLicense } from '../common/info-license';
@@ -53,7 +52,6 @@ import { Operation4xxProblemDetailsRfc7807 } from './operation-4xx-problem-detai
53
52
 
54
53
  export const rules = {
55
54
  spec: OasSpec,
56
- 'info-description': InfoDescription,
57
55
  'info-contact': InfoContact,
58
56
  'info-license': InfoLicense,
59
57
  'info-license-url': InfoLicenseUrl,