Skip to content

Commit 09e657f

Browse files
authored
fix(specs): add a linter to assert that type is present (#4393)
1 parent 4b573fd commit 09e657f

File tree

16 files changed

+185
-18
lines changed

16 files changed

+185
-18
lines changed

.eslintrc.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'automation-custom/no-big-int': 'error',
5555
'automation-custom/no-final-dot': 'error',
5656
'automation-custom/single-quote-ref': 'error',
57+
'automation-custom/has-type': 'error',
5758
},
5859
overrides: [
5960
{

eslint/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { endWithDot } from './rules/endWithDot.js';
2+
import { hasType } from './rules/hasType.js';
23
import { noBigInt } from './rules/noBigInt.js';
34
import { noFinalDot } from './rules/noFinalDot.js';
45
import { noNewLine } from './rules/noNewLine.js';
@@ -10,6 +11,7 @@ import { validInlineTitle } from './rules/validInlineTitle.js';
1011

1112
const rules = {
1213
'end-with-dot': endWithDot,
14+
'has-type': hasType,
1315
'no-big-int': noBigInt,
1416
'no-final-dot': noFinalDot,
1517
'no-new-line': noNewLine,

eslint/src/rules/hasType.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createRule } from 'eslint-plugin-yml/lib/utils';
2+
3+
import { isPairWithKey, isPairWithValue } from '../utils.js';
4+
5+
export const hasType = createRule('hasType', {
6+
meta: {
7+
docs: {
8+
description: '`type` must be specified with `properties` or `items`',
9+
categories: null,
10+
extensionRule: false,
11+
layout: false,
12+
},
13+
messages: {
14+
hasType: '`type` must be specified with `properties` or `items`',
15+
},
16+
type: 'problem',
17+
schema: [],
18+
},
19+
create(context) {
20+
if (!context.getSourceCode().parserServices?.isYAML) {
21+
return {};
22+
}
23+
24+
return {
25+
YAMLPair(node): void {
26+
if (isPairWithKey(node.parent.parent, 'properties')) {
27+
return; // allow everything in properties
28+
}
29+
30+
const type = node.parent.pairs.find((pair) => isPairWithKey(pair, 'type'));
31+
if (isPairWithKey(node, 'properties') && (!type || !isPairWithValue(type, 'object'))) {
32+
return context.report({
33+
node: node as any,
34+
messageId: 'hasType',
35+
});
36+
}
37+
38+
if (isPairWithKey(node, 'items') && (!type || !isPairWithValue(type, 'array'))) {
39+
return context.report({
40+
node: node as any,
41+
messageId: 'hasType',
42+
});
43+
}
44+
},
45+
};
46+
},
47+
});

eslint/src/rules/noBigInt.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ export const noBigInt = createRule('noBigInt', {
3434

3535
// check the format next to the type
3636
node.parent.pairs.find((pair) => {
37-
if (isPairWithKey(pair, 'format') && isScalar(pair.value) && (pair.value.value === 'int32' || pair.value.value === 'int64')) {
37+
if (
38+
isPairWithKey(pair, 'format') &&
39+
isScalar(pair.value) &&
40+
(pair.value.value === 'int32' || pair.value.value === 'int64')
41+
) {
3842
context.report({
3943
node: pair.value as any,
4044
messageId: 'noBigInt',

eslint/src/rules/outOfLineRule.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { RuleModule } from 'eslint-plugin-yml/lib/types.js';
1+
import type { RuleModule } from 'eslint-plugin-yml/lib/types.js';
22
import { createRule } from 'eslint-plugin-yml/lib/utils';
33

4-
import { isNullable, isPairWithKey } from '../utils.js';
4+
import { isBlockScalar, isMapping, isNullable, isPairWithKey, isScalar } from '../utils.js';
55

66
export function createOutOfLineRule({
77
property,
@@ -24,6 +24,8 @@ export function createOutOfLineRule({
2424
},
2525
messages: {
2626
[messageId]: message,
27+
nullDescription: 'description must not be present for `null` type',
28+
descriptionLevel: 'description must not be next to the property',
2729
},
2830
type: 'layout',
2931
schema: [],
@@ -38,6 +40,29 @@ export function createOutOfLineRule({
3840
if (!isPairWithKey(node, property)) {
3941
return;
4042
}
43+
44+
// the 'null' must not have a description otherwise it will generate a model for it
45+
if (
46+
property === 'oneOf' &&
47+
isNullable(node.value) &&
48+
node.value.entries.some(
49+
(entry) =>
50+
isMapping(entry) &&
51+
isPairWithKey(entry.pairs[0], 'type') &&
52+
isScalar(entry.pairs[0].value) &&
53+
!isBlockScalar(entry.pairs[0].value) &&
54+
entry.pairs[0].value.raw === "'null'" &&
55+
entry.pairs.length > 1,
56+
)
57+
) {
58+
context.report({
59+
node: node.value,
60+
messageId: 'nullDescription',
61+
});
62+
63+
return;
64+
}
65+
4166
// parent is mapping, and parent is real parent that must be to the far left
4267
if (node.parent.parent.loc.start.column === 0) {
4368
return;

eslint/src/utils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ export function isPairWithKey(node: AST.YAMLNode | null, key: string): node is A
2626
return isScalar(node.key) && node.key.value === key;
2727
}
2828

29-
export function isNullable(node: AST.YAMLNode | null): boolean {
29+
export function isPairWithValue(node: AST.YAMLNode | null, value: string): node is AST.YAMLPair {
30+
if (node === null || node.type !== 'YAMLPair' || node.value === null) {
31+
return false;
32+
}
33+
return isScalar(node.value) && node.value.value === value;
34+
}
35+
36+
export function isNullable(node: AST.YAMLNode | null): node is AST.YAMLSequence {
3037
return (
3138
isSequence(node) &&
3239
node.entries.some(

eslint/tests/hasType.test.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { runClassic } from 'eslint-vitest-rule-tester';
2+
import yamlParser from 'yaml-eslint-parser';
3+
4+
import { hasType } from '../src/rules/hasType.js';
5+
6+
runClassic(
7+
'has-type',
8+
hasType,
9+
{
10+
valid: [
11+
`
12+
simple:
13+
type: object
14+
properties:
15+
prop1:
16+
`,
17+
`
18+
withArray:
19+
type: array
20+
items:
21+
type: string
22+
`,
23+
],
24+
invalid: [
25+
{
26+
code: `
27+
simple:
28+
properties:
29+
noType:
30+
type: string
31+
`,
32+
errors: [{ messageId: 'hasType' }],
33+
},
34+
{
35+
code: `
36+
wrongType:
37+
type: string
38+
properties:
39+
noType:
40+
type: string
41+
`,
42+
errors: [{ messageId: 'hasType' }],
43+
},
44+
{
45+
code: `
46+
array:
47+
items:
48+
type: string
49+
`,
50+
errors: [{ messageId: 'hasType' }],
51+
},
52+
],
53+
},
54+
{
55+
parser: yamlParser,
56+
},
57+
);

eslint/tests/noBigInt.test.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { runClassic } from 'eslint-vitest-rule-tester';
22
import yamlParser from 'yaml-eslint-parser';
33
import { noBigInt } from '../src/rules/noBigInt.js';
44

5-
65
runClassic(
76
'no-big-int',
87
noBigInt,
98
{
10-
valid: [`
9+
valid: [
10+
`
1111
type: object
1212
properties:
1313
id:
@@ -16,11 +16,13 @@ properties:
1616
url:
1717
type: string
1818
format: uri
19-
`, `
19+
`,
20+
`
2021
prop:
2122
type: integer
2223
format: int32
23-
`],
24+
`,
25+
],
2426
invalid: [
2527
{
2628
code: `

eslint/tests/outOfLineRule.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,32 @@ simple:
105105
`,
106106
errors: [{ messageId: 'oneOfNotOutOfLine' }],
107107
},
108+
{
109+
code: `
110+
simple:
111+
type: object
112+
properties:
113+
name:
114+
oneOf:
115+
- type: string
116+
description: bla bla bla
117+
- type: 'null'
118+
description: bla bla bla
119+
`,
120+
errors: [{ messageId: 'nullDescription' }],
121+
},
122+
{
123+
code: `
124+
root:
125+
oneOf:
126+
oneOf:
127+
- type: string
128+
description: bla bla bla
129+
- type: 'null'
130+
description: bla bla bla
131+
`,
132+
errors: [{ messageId: 'nullDescription' }],
133+
},
108134
],
109135
},
110136
{

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"postinstall": "husky && yarn workspace eslint-plugin-automation-custom build",
2424
"playground:browser": "yarn workspace javascript-browser-playground start",
2525
"scripts:build": "yarn workspace scripts build:actions",
26-
"scripts:lint": "yarn workspace scripts lint",
26+
"scripts:lint": "yarn cli format javascript scripts && yarn cli format javascript eslint",
2727
"scripts:test": "yarn workspace scripts test",
2828
"specs:fix": "eslint --ext=yml $0 --fix",
2929
"specs:lint": "eslint --ext=yml $0",

scripts/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"build:actions": "cd ci/actions/restore-artifacts && esbuild --bundle --format=cjs --minify --platform=node --outfile=builddir/index.cjs --log-level=error src/index.ts",
77
"createGitHubReleases": "yarn runScript ci/codegen/createGitHubReleases.ts",
88
"createMatrix": "yarn runScript ci/githubActions/createMatrix.ts",
9-
"lint": "yarn start format javascript scripts",
109
"lint:deadcode": "knip",
1110
"pre-commit": "node ./ci/husky/pre-commit.mjs",
1211
"pushGeneratedCode": "yarn runScript ci/codegen/pushGeneratedCode.ts",

specs/abtesting/common/schemas/ABTest.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
ABTests:
22
oneOf:
33
- type: array
4-
description: A/B tests.
4+
description: The list of A/B tests, null if no A/B tests are configured for this application.
55
items:
66
$ref: '#/ABTest'
77
- type: 'null'
8-
description: No A/B tests are configured for this application.
98

109
ABTest:
1110
type: object

specs/common/responses/common.yml

+2-4
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,12 @@ updatedAt:
3333
description: Date and time when the object was updated, in RFC 3339 format.
3434

3535
updatedAtNullable:
36-
default: null
3736
oneOf:
3837
- type: string
38+
default: null
39+
description: Date and time when the object was updated, in RFC 3339 format.
3940
example: 2023-07-04T12:49:15Z
40-
description: |
41-
Date and time when the object was updated, in RFC 3339 format.
4241
- type: 'null'
43-
description: If null, this object wasn't updated yet.
4442

4543
deletedAt:
4644
type: string

specs/crawler/common/schemas/configuration.yml

+1
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ requestOptions:
285285
$ref: '#/headers'
286286

287287
waitTime:
288+
type: object
288289
description: Timeout for the HTTP request.
289290
properties:
290291
min:

specs/crawler/common/schemas/getCrawlerResponse.yml

-2
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ BaseResponse:
4141
description: Date and time when the last crawl started, in RFC 3339 format.
4242
example: 2024-04-07T09:16:04Z
4343
- type: 'null'
44-
description: If null, this crawler hasn't indexed anything yet.
4544
lastReindexEndedAt:
4645
default: null
4746
oneOf:
4847
- type: string
4948
description: Date and time when the last crawl finished, in RFC 3339 format.
5049
- type: 'null'
51-
description: If null, this crawler hasn't indexed anything yet.
5250
required:
5351
- name
5452
- createdAt

specs/monitoring/common/schemas/Server.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
title: server
2+
type: object
23
additionalProperties: false
34
properties:
45
name:

0 commit comments

Comments
 (0)