Skip to content

fix(specs): add a linter to assert that type is present #4393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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