Skip to content

Commit

Permalink
fix(specs): add a linter to assert that type is present (#4393)
Browse files Browse the repository at this point in the history
  • Loading branch information
millotp authored Jan 27, 2025
1 parent 4b573fd commit 09e657f
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 18 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {
'automation-custom/no-big-int': 'error',
'automation-custom/no-final-dot': 'error',
'automation-custom/single-quote-ref': 'error',
'automation-custom/has-type': 'error',
},
overrides: [
{
Expand Down
2 changes: 2 additions & 0 deletions eslint/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { endWithDot } from './rules/endWithDot.js';
import { hasType } from './rules/hasType.js';
import { noBigInt } from './rules/noBigInt.js';
import { noFinalDot } from './rules/noFinalDot.js';
import { noNewLine } from './rules/noNewLine.js';
Expand All @@ -10,6 +11,7 @@ import { validInlineTitle } from './rules/validInlineTitle.js';

const rules = {
'end-with-dot': endWithDot,
'has-type': hasType,
'no-big-int': noBigInt,
'no-final-dot': noFinalDot,
'no-new-line': noNewLine,
Expand Down
47 changes: 47 additions & 0 deletions eslint/src/rules/hasType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createRule } from 'eslint-plugin-yml/lib/utils';

import { isPairWithKey, isPairWithValue } from '../utils.js';

export const hasType = createRule('hasType', {
meta: {
docs: {
description: '`type` must be specified with `properties` or `items`',
categories: null,
extensionRule: false,
layout: false,
},
messages: {
hasType: '`type` must be specified with `properties` or `items`',
},
type: 'problem',
schema: [],
},
create(context) {
if (!context.getSourceCode().parserServices?.isYAML) {
return {};
}

return {
YAMLPair(node): void {
if (isPairWithKey(node.parent.parent, 'properties')) {
return; // allow everything in properties
}

const type = node.parent.pairs.find((pair) => isPairWithKey(pair, 'type'));
if (isPairWithKey(node, 'properties') && (!type || !isPairWithValue(type, 'object'))) {
return context.report({
node: node as any,
messageId: 'hasType',
});
}

if (isPairWithKey(node, 'items') && (!type || !isPairWithValue(type, 'array'))) {
return context.report({
node: node as any,
messageId: 'hasType',
});
}
},
};
},
});
6 changes: 5 additions & 1 deletion eslint/src/rules/noBigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export const noBigInt = createRule('noBigInt', {

// check the format next to the type
node.parent.pairs.find((pair) => {
if (isPairWithKey(pair, 'format') && isScalar(pair.value) && (pair.value.value === 'int32' || pair.value.value === 'int64')) {
if (
isPairWithKey(pair, 'format') &&
isScalar(pair.value) &&
(pair.value.value === 'int32' || pair.value.value === 'int64')
) {
context.report({
node: pair.value as any,
messageId: 'noBigInt',
Expand Down
29 changes: 27 additions & 2 deletions eslint/src/rules/outOfLineRule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RuleModule } from 'eslint-plugin-yml/lib/types.js';
import type { RuleModule } from 'eslint-plugin-yml/lib/types.js';
import { createRule } from 'eslint-plugin-yml/lib/utils';

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

export function createOutOfLineRule({
property,
Expand All @@ -24,6 +24,8 @@ export function createOutOfLineRule({
},
messages: {
[messageId]: message,
nullDescription: 'description must not be present for `null` type',
descriptionLevel: 'description must not be next to the property',
},
type: 'layout',
schema: [],
Expand All @@ -38,6 +40,29 @@ export function createOutOfLineRule({
if (!isPairWithKey(node, property)) {
return;
}

// the 'null' must not have a description otherwise it will generate a model for it
if (
property === 'oneOf' &&
isNullable(node.value) &&
node.value.entries.some(
(entry) =>
isMapping(entry) &&
isPairWithKey(entry.pairs[0], 'type') &&
isScalar(entry.pairs[0].value) &&
!isBlockScalar(entry.pairs[0].value) &&
entry.pairs[0].value.raw === "'null'" &&
entry.pairs.length > 1,
)
) {
context.report({
node: node.value,
messageId: 'nullDescription',
});

return;
}

// parent is mapping, and parent is real parent that must be to the far left
if (node.parent.parent.loc.start.column === 0) {
return;
Expand Down
9 changes: 8 additions & 1 deletion eslint/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ export function isPairWithKey(node: AST.YAMLNode | null, key: string): node is A
return isScalar(node.key) && node.key.value === key;
}

export function isNullable(node: AST.YAMLNode | null): boolean {
export function isPairWithValue(node: AST.YAMLNode | null, value: string): node is AST.YAMLPair {
if (node === null || node.type !== 'YAMLPair' || node.value === null) {
return false;
}
return isScalar(node.value) && node.value.value === value;
}

export function isNullable(node: AST.YAMLNode | null): node is AST.YAMLSequence {
return (
isSequence(node) &&
node.entries.some(
Expand Down
57 changes: 57 additions & 0 deletions eslint/tests/hasType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { runClassic } from 'eslint-vitest-rule-tester';
import yamlParser from 'yaml-eslint-parser';

import { hasType } from '../src/rules/hasType.js';

runClassic(
'has-type',
hasType,
{
valid: [
`
simple:
type: object
properties:
prop1:
`,
`
withArray:
type: array
items:
type: string
`,
],
invalid: [
{
code: `
simple:
properties:
noType:
type: string
`,
errors: [{ messageId: 'hasType' }],
},
{
code: `
wrongType:
type: string
properties:
noType:
type: string
`,
errors: [{ messageId: 'hasType' }],
},
{
code: `
array:
items:
type: string
`,
errors: [{ messageId: 'hasType' }],
},
],
},
{
parser: yamlParser,
},
);
10 changes: 6 additions & 4 deletions eslint/tests/noBigInt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { runClassic } from 'eslint-vitest-rule-tester';
import yamlParser from 'yaml-eslint-parser';
import { noBigInt } from '../src/rules/noBigInt.js';


runClassic(
'no-big-int',
noBigInt,
{
valid: [`
valid: [
`
type: object
properties:
id:
Expand All @@ -16,11 +16,13 @@ properties:
url:
type: string
format: uri
`, `
`,
`
prop:
type: integer
format: int32
`],
`,
],
invalid: [
{
code: `
Expand Down
26 changes: 26 additions & 0 deletions eslint/tests/outOfLineRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ simple:
`,
errors: [{ messageId: 'oneOfNotOutOfLine' }],
},
{
code: `
simple:
type: object
properties:
name:
oneOf:
- type: string
description: bla bla bla
- type: 'null'
description: bla bla bla
`,
errors: [{ messageId: 'nullDescription' }],
},
{
code: `
root:
oneOf:
oneOf:
- type: string
description: bla bla bla
- type: 'null'
description: bla bla bla
`,
errors: [{ messageId: 'nullDescription' }],
},
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"postinstall": "husky && yarn workspace eslint-plugin-automation-custom build",
"playground:browser": "yarn workspace javascript-browser-playground start",
"scripts:build": "yarn workspace scripts build:actions",
"scripts:lint": "yarn workspace scripts lint",
"scripts:lint": "yarn cli format javascript scripts && yarn cli format javascript eslint",
"scripts:test": "yarn workspace scripts test",
"specs:fix": "eslint --ext=yml $0 --fix",
"specs:lint": "eslint --ext=yml $0",
Expand Down
1 change: 0 additions & 1 deletion scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"build:actions": "cd ci/actions/restore-artifacts && esbuild --bundle --format=cjs --minify --platform=node --outfile=builddir/index.cjs --log-level=error src/index.ts",
"createGitHubReleases": "yarn runScript ci/codegen/createGitHubReleases.ts",
"createMatrix": "yarn runScript ci/githubActions/createMatrix.ts",
"lint": "yarn start format javascript scripts",
"lint:deadcode": "knip",
"pre-commit": "node ./ci/husky/pre-commit.mjs",
"pushGeneratedCode": "yarn runScript ci/codegen/pushGeneratedCode.ts",
Expand Down
3 changes: 1 addition & 2 deletions specs/abtesting/common/schemas/ABTest.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
ABTests:
oneOf:
- type: array
description: A/B tests.
description: The list of A/B tests, null if no A/B tests are configured for this application.
items:
$ref: '#/ABTest'
- type: 'null'
description: No A/B tests are configured for this application.

ABTest:
type: object
Expand Down
6 changes: 2 additions & 4 deletions specs/common/responses/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ updatedAt:
description: Date and time when the object was updated, in RFC 3339 format.

updatedAtNullable:
default: null
oneOf:
- type: string
default: null
description: Date and time when the object was updated, in RFC 3339 format.
example: 2023-07-04T12:49:15Z
description: |
Date and time when the object was updated, in RFC 3339 format.
- type: 'null'
description: If null, this object wasn't updated yet.

deletedAt:
type: string
Expand Down
1 change: 1 addition & 0 deletions specs/crawler/common/schemas/configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ requestOptions:
$ref: '#/headers'

waitTime:
type: object
description: Timeout for the HTTP request.
properties:
min:
Expand Down
2 changes: 0 additions & 2 deletions specs/crawler/common/schemas/getCrawlerResponse.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@ BaseResponse:
description: Date and time when the last crawl started, in RFC 3339 format.
example: 2024-04-07T09:16:04Z
- type: 'null'
description: If null, this crawler hasn't indexed anything yet.
lastReindexEndedAt:
default: null
oneOf:
- type: string
description: Date and time when the last crawl finished, in RFC 3339 format.
- type: 'null'
description: If null, this crawler hasn't indexed anything yet.
required:
- name
- createdAt
Expand Down
1 change: 1 addition & 0 deletions specs/monitoring/common/schemas/Server.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
title: server
type: object
additionalProperties: false
properties:
name:
Expand Down

0 comments on commit 09e657f

Please sign in to comment.