diff --git a/specification/_types/Geo.ts b/specification/_types/Geo.ts index d5f8c5db53..66856a5115 100644 --- a/specification/_types/Geo.ts +++ b/specification/_types/Geo.ts @@ -18,7 +18,7 @@ */ import { UserDefinedValue } from '@spec_utils/UserDefinedValue' -import { double } from './Numeric' +import { double, integer } from './Numeric' export class DistanceParsed { precision: double @@ -81,13 +81,13 @@ export enum GeoShapeRelation { contains } -export type GeoTilePrecision = number +export type GeoTilePrecision = integer /** * A precision that can be expressed as a geohash length between 1 and 12, or a distance measure like "1km", "10m". * @codegen_names geohash_length, distance */ -export type GeoHashPrecision = number | string +export type GeoHashPrecision = integer | string export type GeoHash = string /** A map tile reference, represented as `{zoom}/{x}/{y}` */ diff --git a/specification/eslint.config.js b/specification/eslint.config.js index 6524736668..ad85b7f178 100644 --- a/specification/eslint.config.js +++ b/specification/eslint.config.js @@ -34,6 +34,7 @@ export default defineConfig({ 'es-spec-validator/single-key-dictionary-key-is-string': 'error', 'es-spec-validator/dictionary-key-is-string': 'error', 'es-spec-validator/no-native-types': 'error', - 'es-spec-validator/invalid-node-types': 'error' + 'es-spec-validator/invalid-node-types': 'error', + 'es-spec-validator/no-generic-number': 'error' } }) diff --git a/specification/indices/get_alias/_types/response.ts b/specification/indices/get_alias/_types/response.ts index 99ba788fe7..3b427e3394 100644 --- a/specification/indices/get_alias/_types/response.ts +++ b/specification/indices/get_alias/_types/response.ts @@ -17,6 +17,7 @@ * under the License. */ +import { integer } from '@_types/Numeric' import { AliasDefinition } from '@indices/_types/AliasDefinition' import { AdditionalProperties } from '@spec_utils/behaviors' import { Dictionary } from '@spec_utils/Dictionary' @@ -32,5 +33,5 @@ export class NotFoundAliases implements AdditionalProperties { error: string - status: number + status: integer } diff --git a/validator/README.md b/validator/README.md index 5b57085972..bfc886feb1 100644 --- a/validator/README.md +++ b/validator/README.md @@ -11,6 +11,7 @@ It is configured [in the specification directory](../specification/eslint.config | `dictionary-key-is-string` | `Dictionary` keys must be strings. | | `no-native-types` | `Typescript native types not allowed, use aliases. | | `invalid-node-types` | The spec uses a subset of TypeScript, so some types, clauses and expressions are not allowed. | +| `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | ## Usage diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index aba3042515..a823ed46ee 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -20,6 +20,7 @@ import singleKeyDict from './rules/single-key-dictionary-key-is-string.js' import dict from './rules/dictionary-key-is-string.js' import noNativeTypes from './rules/no-native-types.js' import invalidNodeTypes from './rules/invalid-node-types.js' +import noGenericNumber from './rules/no-generic-number.js' export default { rules: { @@ -27,5 +28,6 @@ export default { 'dictionary-key-is-string': dict, 'no-native-types': noNativeTypes, 'invalid-node-types': invalidNodeTypes, + 'no-generic-number': noGenericNumber, } } diff --git a/validator/rules/no-generic-number.js b/validator/rules/no-generic-number.js new file mode 100644 index 0000000000..c18ecbd26e --- /dev/null +++ b/validator/rules/no-generic-number.js @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ESLintUtils } from '@typescript-eslint/utils'; +import * as path from 'path'; + +const createRule = ESLintUtils.RuleCreator(name => `https://example.com/rule/${name}`) + +export default createRule({ + name: 'no-generic-number', + create(context) { + return { + TSNumberKeyword(node) { + const filename = context.filename || context.getFilename(); + const normalizedPath = path.normalize(filename); + + // allow number only in _types/Numeric.ts + if (normalizedPath.includes(path.join('_types', 'Numeric.ts'))) { + return; + } + + context.report({ + node, + messageId: 'noGenericNumber', + data: { + types: 'short, byte, integer, uint, long, ulong, float, or double' + } + }) + }, + } + }, + meta: { + docs: { + description: 'Force usage of concrete numeric types instead of generic "number" type', + }, + messages: { + noGenericNumber: 'Generic "number" type is not allowed. Use concrete numeric types instead: {{types}}. See specification/_types/Numeric.ts for available types.' + }, + type: 'problem', + schema: [] + }, + defaultOptions: [] +}) + diff --git a/validator/test/no-generic-number.test.js b/validator/test/no-generic-number.test.js new file mode 100644 index 0000000000..8b9a48d3d7 --- /dev/null +++ b/validator/test/no-generic-number.test.js @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { RuleTester } from '@typescript-eslint/rule-tester' +import rule from '../rules/no-generic-number.js' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts*'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, +}) + +ruleTester.run('no-generic-number', rule, { + valid: [ + `type MyType = { count: integer }`, + `type MyType = { amount: long }`, + `type MyType = { price: float }`, + `type MyType = { value: double }`, + `type MyType = { small: short }`, + `type MyType = { tiny: byte }`, + `type MyType = { unsigned: uint }`, + `type MyType = { bigUnsigned: ulong }`, + `class MyClass { score: float; count: integer; }`, + `interface MyInterface { id: long; ratio: double; }`, + `type MyType = { value: integer | string }`, + `type MyType = { id: long | float }`, + `type MyType = { numbers: integer[] }`, + `type MyType = { values: Array }`, + `type MyType = Dictionary`, + ], + invalid: [ + { + code: `type MyType = { count: number }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `class MyClass { status: number }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `interface MyInterface { id: number }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `type MyType = { value: string | number }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `type MyType = { items: number[] }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `type MyType = { items: Array }`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `type MyType = Dictionary`, + errors: [{ messageId: 'noGenericNumber' }] + }, + { + code: `export class Response { body: { count: number } }`, + errors: [{ messageId: 'noGenericNumber' }] + } + ], +}) +