diff --git a/.eslintrc.cjs b/.eslintrc.cjs index eaa733dbe37..1ef8c865350 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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: [ { diff --git a/eslint/src/index.ts b/eslint/src/index.ts index ebe64eb5f81..7fabf71ed69 100644 --- a/eslint/src/index.ts +++ b/eslint/src/index.ts @@ -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'; @@ -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, diff --git a/eslint/src/rules/hasType.ts b/eslint/src/rules/hasType.ts new file mode 100644 index 00000000000..c2ee06ab1ba --- /dev/null +++ b/eslint/src/rules/hasType.ts @@ -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', + }); + } + }, + }; + }, +}); diff --git a/eslint/src/rules/noBigInt.ts b/eslint/src/rules/noBigInt.ts index 535c84da425..8b61d5682ae 100644 --- a/eslint/src/rules/noBigInt.ts +++ b/eslint/src/rules/noBigInt.ts @@ -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', diff --git a/eslint/src/rules/outOfLineRule.ts b/eslint/src/rules/outOfLineRule.ts index 565f2b1a66a..06c3468d381 100644 --- a/eslint/src/rules/outOfLineRule.ts +++ b/eslint/src/rules/outOfLineRule.ts @@ -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, @@ -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: [], @@ -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; diff --git a/eslint/src/utils.ts b/eslint/src/utils.ts index da6a493758c..2c6c06a4477 100644 --- a/eslint/src/utils.ts +++ b/eslint/src/utils.ts @@ -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( diff --git a/eslint/tests/hasType.test.ts b/eslint/tests/hasType.test.ts new file mode 100644 index 00000000000..536adcccac7 --- /dev/null +++ b/eslint/tests/hasType.test.ts @@ -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, + }, +); diff --git a/eslint/tests/noBigInt.test.ts b/eslint/tests/noBigInt.test.ts index ed5c00b3654..0c413c1a5c2 100644 --- a/eslint/tests/noBigInt.test.ts +++ b/eslint/tests/noBigInt.test.ts @@ -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: @@ -16,11 +16,13 @@ properties: url: type: string format: uri -`, ` +`, + ` prop: type: integer format: int32 -`], +`, + ], invalid: [ { code: ` diff --git a/eslint/tests/outOfLineRule.test.ts b/eslint/tests/outOfLineRule.test.ts index bc08007a93a..8790c18802c 100644 --- a/eslint/tests/outOfLineRule.test.ts +++ b/eslint/tests/outOfLineRule.test.ts @@ -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' }], + }, ], }, { diff --git a/package.json b/package.json index f465b054bfb..cf871767558 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/package.json b/scripts/package.json index 24da5862e9b..5d5e1674556 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -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", diff --git a/specs/abtesting/common/schemas/ABTest.yml b/specs/abtesting/common/schemas/ABTest.yml index a769d098a06..944c180fb0d 100644 --- a/specs/abtesting/common/schemas/ABTest.yml +++ b/specs/abtesting/common/schemas/ABTest.yml @@ -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 diff --git a/specs/common/responses/common.yml b/specs/common/responses/common.yml index 645f27bb30b..e5853f91370 100644 --- a/specs/common/responses/common.yml +++ b/specs/common/responses/common.yml @@ -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 diff --git a/specs/crawler/common/schemas/configuration.yml b/specs/crawler/common/schemas/configuration.yml index 58e8fa797c3..fd529e4d1d7 100644 --- a/specs/crawler/common/schemas/configuration.yml +++ b/specs/crawler/common/schemas/configuration.yml @@ -285,6 +285,7 @@ requestOptions: $ref: '#/headers' waitTime: + type: object description: Timeout for the HTTP request. properties: min: diff --git a/specs/crawler/common/schemas/getCrawlerResponse.yml b/specs/crawler/common/schemas/getCrawlerResponse.yml index 745ac308158..b1591ecbf71 100644 --- a/specs/crawler/common/schemas/getCrawlerResponse.yml +++ b/specs/crawler/common/schemas/getCrawlerResponse.yml @@ -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 diff --git a/specs/monitoring/common/schemas/Server.yml b/specs/monitoring/common/schemas/Server.yml index 5559c89a528..a104922f6b1 100644 --- a/specs/monitoring/common/schemas/Server.yml +++ b/specs/monitoring/common/schemas/Server.yml @@ -1,4 +1,5 @@ title: server +type: object additionalProperties: false properties: name: