diff --git a/specification/_global/delete/DeleteResponse.ts b/specification/_global/delete/DeleteResponse.ts index ab82380f5a..d47d4b13e8 100644 --- a/specification/_global/delete/DeleteResponse.ts +++ b/specification/_global/delete/DeleteResponse.ts @@ -29,6 +29,7 @@ export class Response { * the response is the same as the successful case, but with a 404. */ statusCodes: [404] + // eslint-disable-next-line es-spec-validator/no-inline-unions, es-spec-validator/prefer-tagged-variants -- TODO: use tagged variant body: WriteResponseBase | ErrorResponseBase } ] diff --git a/specification/_global/get/GetResponse.ts b/specification/_global/get/GetResponse.ts index 043c74e0c8..5f3fde1ab7 100644 --- a/specification/_global/get/GetResponse.ts +++ b/specification/_global/get/GetResponse.ts @@ -29,6 +29,7 @@ export class Response { // * index_not_found_exception as an error if the index doesn't exist // * GetResult with only the requested _id, _index properties and found as a false boolean statusCodes: [404] + // eslint-disable-next-line es-spec-validator/no-inline-unions, es-spec-validator/prefer-tagged-variants -- TODO: use tagged variant body: GetResult | ErrorResponseBase } ] diff --git a/specification/_global/scripts_painless_execute/types.ts b/specification/_global/scripts_painless_execute/types.ts index b032fb379a..7b1c80bfbd 100644 --- a/specification/_global/scripts_painless_execute/types.ts +++ b/specification/_global/scripts_painless_execute/types.ts @@ -97,5 +97,6 @@ export class PainlessScript { * * `long`: `emit(long)` * * `keyword`: `emit(String)` */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias emit: boolean | DateTime | double | string | Ip | long } diff --git a/specification/_global/search/_types/hits.ts b/specification/_global/search/_types/hits.ts index 17c7291618..f1c6f97aec 100644 --- a/specification/_global/search/_types/hits.ts +++ b/specification/_global/search/_types/hits.ts @@ -52,6 +52,7 @@ export class Hit { fields?: Dictionary highlight?: Dictionary inner_hits?: Dictionary + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias matched_queries?: string[] | Dictionary _nested?: NestedIdentity _ignored?: string[] @@ -69,6 +70,7 @@ export class Hit { export class HitsMetadata { /** Total hit count information, present only if `track_total_hits` wasn't `false` in the search request. */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias total?: TotalHits | long hits: Hit[] diff --git a/specification/_global/search/_types/suggester.ts b/specification/_global/search/_types/suggester.ts index 7dbbb7da99..d4ab25ca92 100644 --- a/specification/_global/search/_types/suggester.ts +++ b/specification/_global/search/_types/suggester.ts @@ -186,6 +186,7 @@ export class RegexOptions { * Optional operators for the regular expression. * @doc_id regexp-syntax */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias flags?: integer | string /** * Maximum number of automaton states required for the query. diff --git a/specification/_types/mapping/specialized.ts b/specification/_types/mapping/specialized.ts index 451d3b7cc3..140f7966b8 100644 --- a/specification/_types/mapping/specialized.ts +++ b/specification/_types/mapping/specialized.ts @@ -44,6 +44,7 @@ export class SuggestContext { name: Name path?: Field type: string + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias precision?: integer | string } diff --git a/specification/_types/query_dsl/compound.ts b/specification/_types/query_dsl/compound.ts index 434f5918fc..26f1463e57 100644 --- a/specification/_types/query_dsl/compound.ts +++ b/specification/_types/query_dsl/compound.ts @@ -143,6 +143,7 @@ export class ScriptScoreFunction { export class RandomScoreFunction { field?: Field + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias seed?: long | string } diff --git a/specification/_types/query_dsl/fulltext.ts b/specification/_types/query_dsl/fulltext.ts index 0885a508d3..ab04d65828 100644 --- a/specification/_types/query_dsl/fulltext.ts +++ b/specification/_types/query_dsl/fulltext.ts @@ -395,6 +395,7 @@ export class MatchQuery extends QueryBase { */ // FIXME: docs states "date" as a possible format. Add DateMath, or DurationLarge? // Should also be consistent with MultiMatchQuery.query + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias query: string | float | boolean /** * Indicates whether no documents are returned if the `analyzer` removes all tokens, such as when using a `stop` filter. diff --git a/specification/_types/query_dsl/term.ts b/specification/_types/query_dsl/term.ts index 77b8aa6b7d..ad4e006274 100644 --- a/specification/_types/query_dsl/term.ts +++ b/specification/_types/query_dsl/term.ts @@ -82,6 +82,7 @@ export class FuzzyQuery extends QueryBase { // ES is lenient and accepts any primitive type, but ultimately converts it to a string. // Changing this field definition from UserDefinedValue to string breaks a recording produced from Nest tests, // but Nest is probably also overly flexible here and exposes an option that should not exist. + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias value: string | double | boolean } diff --git a/specification/eslint.config.js b/specification/eslint.config.js index c13abf7393..c2a96a6368 100644 --- a/specification/eslint.config.js +++ b/specification/eslint.config.js @@ -37,6 +37,8 @@ export default defineConfig({ 'es-spec-validator/invalid-node-types': 'error', 'es-spec-validator/no-generic-number': 'error', 'es-spec-validator/request-must-have-urls': 'error', - 'es-spec-validator/no-variants-on-responses': 'error' + 'es-spec-validator/no-variants-on-responses': 'error', + 'es-spec-validator/no-inline-unions': 'error', + 'es-spec-validator/prefer-tagged-variants': 'error' } }) diff --git a/specification/fleet/global_checkpoints/GlobalCheckpointsRequest.ts b/specification/fleet/global_checkpoints/GlobalCheckpointsRequest.ts index 7919b91026..f58a91b482 100644 --- a/specification/fleet/global_checkpoints/GlobalCheckpointsRequest.ts +++ b/specification/fleet/global_checkpoints/GlobalCheckpointsRequest.ts @@ -43,6 +43,7 @@ export interface Request extends RequestBase { /** * A single index or index alias that resolves to a single index. */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias index: IndexName | IndexAlias } query_parameters: { diff --git a/specification/fleet/msearch/MultiSearchRequest.ts b/specification/fleet/msearch/MultiSearchRequest.ts index f8c40fc5b2..2820ead05e 100644 --- a/specification/fleet/msearch/MultiSearchRequest.ts +++ b/specification/fleet/msearch/MultiSearchRequest.ts @@ -54,6 +54,7 @@ export interface Request extends RequestBase { /** * A single target to search. If the target is an index alias, it must resolve to a single index. */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias index?: IndexName | IndexAlias } query_parameters: { diff --git a/specification/fleet/search/SearchRequest.ts b/specification/fleet/search/SearchRequest.ts index 6e38bfe94e..a913986105 100644 --- a/specification/fleet/search/SearchRequest.ts +++ b/specification/fleet/search/SearchRequest.ts @@ -72,6 +72,7 @@ export interface Request extends RequestBase { /** * A single target to search. If the target is an index alias, it must resolve to a single index. */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias index: IndexName | IndexAlias } query_parameters: { diff --git a/specification/indices/_types/IndexRouting.ts b/specification/indices/_types/IndexRouting.ts index 32db133db0..8b7893428f 100644 --- a/specification/indices/_types/IndexRouting.ts +++ b/specification/indices/_types/IndexRouting.ts @@ -60,5 +60,6 @@ export class IndexRoutingAllocationInitialRecovery { // ES: DiskThresholdSettings export class IndexRoutingAllocationDisk { + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias threshold_enabled?: boolean | string } diff --git a/specification/indices/_types/IndexSettings.ts b/specification/indices/_types/IndexSettings.ts index 9705a7c786..4f050b0f40 100644 --- a/specification/indices/_types/IndexSettings.ts +++ b/specification/indices/_types/IndexSettings.ts @@ -84,11 +84,13 @@ export class IndexSettings * @server_default 1 * @availability stack * */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias number_of_shards?: integer | string // TODO: should be only int /** * @server_default 0 * @availability stack * */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias number_of_replicas?: integer | string // TODO: should be only int number_of_routing_shards?: integer /** @server_default false */ @@ -100,6 +102,7 @@ export class IndexSettings /** @server_default true */ load_fixed_bitset_filters_eagerly?: boolean /** @server_default false */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias hidden?: boolean | string // TODO should be bool only /** @server_default false */ auto_expand_replicas?: WithNullValue @@ -147,11 +150,14 @@ export class IndexSettings creation_date_string?: DateTime uuid?: Uuid version?: IndexVersioning + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias verified_before_close?: boolean | string + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias format?: string | integer max_slices_per_scroll?: integer translog?: Translog query_string?: SettingsQueryString + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias priority?: integer | string top_metrics_max_size?: integer analysis?: IndexSettingsAnalysis @@ -332,6 +338,7 @@ export class IndexSettingsLifecycle { * applicable for an index). * @server_default true */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias prefer_ilm?: boolean | string } @@ -448,6 +455,7 @@ export class MappingLimitSettings { field_name_length?: MappingLimitSettingsFieldNameLength dimension_fields?: MappingLimitSettingsDimensionFields source?: MappingLimitSettingsSourceFields + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias ignore_malformed?: boolean | string } @@ -458,6 +466,7 @@ export class MappingLimitSettingsTotalFields { * degradations and memory issues, especially in clusters with a high load or few resources. * @server_default 1000 */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias limit?: long | string /** * This setting determines what happens when a dynamically mapped field would exceed the total fields limit. When set @@ -467,6 +476,7 @@ export class MappingLimitSettingsTotalFields { * The fields that were not added to the mapping will be added to the _ignored field. * @server_default false */ + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias ignore_dynamic_beyond_limit?: boolean | string } diff --git a/specification/indices/get_alias/IndicesGetAliasResponse.ts b/specification/indices/get_alias/IndicesGetAliasResponse.ts index 0c3831e196..83bf0c50c7 100644 --- a/specification/indices/get_alias/IndicesGetAliasResponse.ts +++ b/specification/indices/get_alias/IndicesGetAliasResponse.ts @@ -31,6 +31,7 @@ export class Response { exceptions: [ { statusCodes: [404] + // eslint-disable-next-line es-spec-validator/no-inline-unions, es-spec-validator/prefer-tagged-variants -- TODO: use tagged variant body: NotFoundAliases | ErrorResponseBase } ] diff --git a/specification/inference/completion/CompletionRequest.ts b/specification/inference/completion/CompletionRequest.ts index 79cedf6035..d255ec3015 100644 --- a/specification/inference/completion/CompletionRequest.ts +++ b/specification/inference/completion/CompletionRequest.ts @@ -60,7 +60,7 @@ export interface Request extends RequestBase { * Inference input. * Either a string or an array of strings. */ - input: string | Array + input: string | string[] /** * Task settings for the individual inference request. These settings are specific to the you specified and override the task settings specified when initializing the service. */ diff --git a/specification/inference/inference/InferenceRequest.ts b/specification/inference/inference/InferenceRequest.ts index cb9e9487f3..e4d063f08a 100644 --- a/specification/inference/inference/InferenceRequest.ts +++ b/specification/inference/inference/InferenceRequest.ts @@ -81,7 +81,7 @@ export interface Request extends RequestBase { * > info * > Inference endpoints for the `completion` task type currently only support a single string as input. */ - input: string | Array + input: string | string[] /** * Specifies the input data type for the text embedding model. The `input_type` parameter only applies to Inference Endpoints with the `text_embedding` task type. Possible values include: * * `SEARCH` diff --git a/specification/inference/sparse_embedding/SparseEmbeddingRequest.ts b/specification/inference/sparse_embedding/SparseEmbeddingRequest.ts index 7efe91dd82..0775c718ff 100644 --- a/specification/inference/sparse_embedding/SparseEmbeddingRequest.ts +++ b/specification/inference/sparse_embedding/SparseEmbeddingRequest.ts @@ -54,7 +54,7 @@ export interface Request extends RequestBase { * Inference input. * Either a string or an array of strings. */ - input: string | Array + input: string | string[] /** * Task settings for the individual inference request. These settings are specific to the you specified and override the task settings specified when initializing the service. */ diff --git a/specification/inference/text_embedding/TextEmbeddingRequest.ts b/specification/inference/text_embedding/TextEmbeddingRequest.ts index ef1a537b26..dacda5e23d 100644 --- a/specification/inference/text_embedding/TextEmbeddingRequest.ts +++ b/specification/inference/text_embedding/TextEmbeddingRequest.ts @@ -54,7 +54,7 @@ export interface Request extends RequestBase { * Inference input. * Either a string or an array of strings. */ - input: string | Array + input: string | string[] /** * The input data type for the text embedding model. Possible values include: * * `SEARCH` diff --git a/specification/nodes/info/types.ts b/specification/nodes/info/types.ts index 7c4473a4dc..26bedf6f25 100644 --- a/specification/nodes/info/types.ts +++ b/specification/nodes/info/types.ts @@ -152,6 +152,7 @@ export class NodeInfoSettingsCluster { } export class DeprecationIndexing { + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias enabled: boolean | string } @@ -202,7 +203,9 @@ export class NodeInfoClient { export class NodeInfoSettingsHttp { type: NodeInfoSettingsHttpType 'type.default'?: string // TODO this clashes with NodeInfoSettingsHttpType + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias compression?: boolean | string + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias port?: integer | string } @@ -376,6 +379,7 @@ export class NodeJvmInfo { vm_vendor: string vm_version: VersionString using_bundled_jdk: boolean + // eslint-disable-next-line es-spec-validator/no-inline-unions -- TODO: create named alias using_compressed_ordinary_object_pointers?: boolean | string input_arguments: string[] } diff --git a/validator/README.md b/validator/README.md index 13274bc86f..6bb626222f 100644 --- a/validator/README.md +++ b/validator/README.md @@ -14,6 +14,8 @@ It is configured [in the specification directory](../specification/eslint.config | `no-generic-number` | Generic `number` type is not allowed outside of `_types/Numeric.ts`. Use concrete numeric types like `integer`, `long`, `float`, `double`, etc. | | `request-must-have-urls` | All Request interfaces extending `RequestBase` must have a `urls` property defining their endpoint paths and HTTP methods. | | `no-variants-on-responses` | `@variants` is only supported on Interface types, not on Request or Response classes. Use value_body pattern with `@codegen_name` instead. | +| `no-inline-unions` | Inline union types (e.g., `field: A \| B`) are not allowed in properties/fields. Define a named type alias instead to improve code generation for statically-typed languages. | +| `prefer-tagged-variants` | Union of class types should use tagged variants (`@variants internal` or `@variants container`) instead of inline unions for better deserialization support in statically-typed languages. | ## Usage diff --git a/validator/eslint-plugin-es-spec.js b/validator/eslint-plugin-es-spec.js index 74fa1fc944..73b3a0f2b9 100644 --- a/validator/eslint-plugin-es-spec.js +++ b/validator/eslint-plugin-es-spec.js @@ -23,6 +23,8 @@ import invalidNodeTypes from './rules/invalid-node-types.js' import noGenericNumber from './rules/no-generic-number.js' import requestMustHaveUrls from './rules/request-must-have-urls.js' import noVariantsOnResponses from './rules/no-variants-on-responses.js' +import noInlineUnions from './rules/no-inline-unions.js' +import preferTaggedVariants from './rules/prefer-tagged-variants.js' export default { rules: { @@ -33,5 +35,7 @@ export default { 'no-generic-number': noGenericNumber, 'request-must-have-urls': requestMustHaveUrls, 'no-variants-on-responses': noVariantsOnResponses, + 'no-inline-unions': noInlineUnions, + 'prefer-tagged-variants': preferTaggedVariants, } } diff --git a/validator/rules/no-inline-unions.js b/validator/rules/no-inline-unions.js new file mode 100644 index 0000000000..93fd54c251 --- /dev/null +++ b/validator/rules/no-inline-unions.js @@ -0,0 +1,106 @@ +/* + * 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'; + +const createRule = ESLintUtils.RuleCreator(name => `https://example.com/rule/${name}`) + +export default createRule({ + name: 'no-inline-unions', + create(context) { + return { + TSUnionType(node) { + const parent = node.parent; + + const isPropertyAnnotation = + parent?.type === 'TSTypeAnnotation' && + parent.parent?.type === 'PropertyDefinition'; + + const isInterfaceProperty = + parent?.type === 'TSTypeAnnotation' && + parent.parent?.type === 'TSPropertySignature'; + + if (!isPropertyAnnotation && !isInterfaceProperty) { + return; + } + + if (node.types.length === 2) { + const [first, second] = node.types; + + // skip Type | null or Type | undefined + const hasNullOrUndefined = + first.type === 'TSNullKeyword' || first.type === 'TSUndefinedKeyword' || + second.type === 'TSNullKeyword' || second.type === 'TSUndefinedKeyword'; + if (hasNullOrUndefined) { + return; + } + + // skip Type | Type[] pattern + if (second.type === 'TSArrayType') { + if (first.type === second.elementType?.type) { + if (first.type === 'TSTypeReference' && second.elementType.type === 'TSTypeReference') { + // for reference types, check names match + if (first.typeName?.name === second.elementType.typeName?.name) { + return; + } + } else if (first.type === second.elementType.type) { + // for primitive types (string, number, etc.), types already match + return; + } + } + } + + // skip Type[] | Type pattern + if (first.type === 'TSArrayType') { + if (first.elementType?.type === second.type) { + if (first.elementType.type === 'TSTypeReference' && second.type === 'TSTypeReference') { + // for reference types, check names match + if (first.elementType.typeName?.name === second.typeName?.name) { + return; + } + } else if (first.elementType.type === second.type) { + // for primitive types (string, number, etc.), types already match + return; + } + } + } + } + + context.report({ + node, + messageId: 'noInlineUnion', + data: { + suggestion: 'Define a named type alias (e.g., "export type MyUnion = A | B") and use that type instead' + } + }) + }, + } + }, + meta: { + docs: { + description: 'Inline union types are not allowed in properties/fields. Use a named type alias instead to improve code generation for statically-typed languages.', + }, + messages: { + noInlineUnion: 'Inline union types are not allowed. {{suggestion}}.' + }, + type: 'problem', + schema: [] + }, + defaultOptions: [] +}) + diff --git a/validator/rules/prefer-tagged-variants.js b/validator/rules/prefer-tagged-variants.js new file mode 100644 index 0000000000..4c265c8e79 --- /dev/null +++ b/validator/rules/prefer-tagged-variants.js @@ -0,0 +1,128 @@ +/* + * 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, TSESTree } from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator(name => `https://example.com/rule/${name}`) + +/** + * Heuristic to identify class types by checking if the type name is PascalCase + * and not a known primitive or utility type. + */ +function isLikelyClassType(typeNode) { + if (typeNode.type === 'TSTypeReference' && typeNode.typeName.type === 'Identifier') { + const name = typeNode.typeName.name; + + const knownNonClassTypes = [ + 'string', 'number', 'boolean', 'null', 'undefined', + 'integer', 'long', 'short', 'byte', 'float', 'double', 'uint', 'ulong', + 'Array', 'Dictionary', 'SingleKeyDictionary', 'UserDefinedValue', + 'Field', 'Id', 'IndexName', 'Name' + ]; + + if (knownNonClassTypes.includes(name)) { + return false; + } + + const hasUpperStart = /^[A-Z]/.test(name); + const hasLowercase = /[a-z]/.test(name); + + return hasUpperStart && hasLowercase; + } + + return false; +} + +export default createRule({ + name: 'prefer-tagged-variants', + create(context) { + return { + TSUnionType(node) { + const parent = node.parent; + + const isPropertyAnnotation = + parent?.type === 'TSTypeAnnotation' && + parent.parent?.type === 'PropertyDefinition'; + + const isInterfaceProperty = + parent?.type === 'TSTypeAnnotation' && + parent.parent?.type === 'TSPropertySignature'; + + if (!isPropertyAnnotation && !isInterfaceProperty) { + return; + } + + // skip Type | Type[] pattern + if (node.types.length === 2) { + const [first, second] = node.types; + + // check Type | Type[] + if (second.type === 'TSArrayType' && + first.type === 'TSTypeReference' && + second.elementType?.type === 'TSTypeReference') { + const firstName = first.typeName?.name; + const secondName = second.elementType.typeName?.name; + if (firstName && firstName === secondName) { + return; + } + } + + // check Type[] | Type + if (first.type === 'TSArrayType' && + first.elementType?.type === 'TSTypeReference' && + second.type === 'TSTypeReference') { + const firstName = first.elementType.typeName?.name; + const secondName = second.typeName?.name; + if (firstName && firstName === secondName) { + return; + } + } + } + + const allMembersAreClasses = node.types.every(typeNode => { + if (typeNode.type === 'TSArrayType') { + return isLikelyClassType(typeNode.elementType); + } + return isLikelyClassType(typeNode); + }); + + if (allMembersAreClasses && node.types.length >= 2) { + context.report({ + node, + messageId: 'preferTaggedVariants', + data: { + suggestion: 'Use tagged variants with @variants internal or @variants container (external). See modeling guide: https://github.com/elastic/elasticsearch-specification/blob/main/docs/modeling-guide.md#variants' + } + }) + } + }, + } + }, + meta: { + docs: { + description: 'Union of class types should use tagged variants (internal or external) instead of inline unions for better code generation in statically-typed languages.', + }, + messages: { + preferTaggedVariants: 'Union of class types is not allowed. {{suggestion}}.' + }, + type: 'problem', + schema: [] + }, + defaultOptions: [] +}) + diff --git a/validator/test/no-inline-unions.test.js b/validator/test/no-inline-unions.test.js new file mode 100644 index 0000000000..9e3e7c1958 --- /dev/null +++ b/validator/test/no-inline-unions.test.js @@ -0,0 +1,95 @@ +/* + * 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-inline-unions.js' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts*'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, +}) + +ruleTester.run('no-inline-unions', rule, { + valid: [ + `export type MyUnion = string | number`, + `export type Result = Success | Error`, + `type Status = 'active' | 'inactive' | 'pending'`, + `type InputType = string | string[] + class MyClass { + input: InputType + }`, + `type QueryOrArray = QueryContainer | QueryContainer[] + interface MyInterface { + filter?: QueryOrArray + }`, + `class MyClass { + name: string + count: integer + items: string[] + }`, + `type Item = string | number + class MyClass { + items: Array + }`, + `class MyClass { + id: string | null + }`, + `interface Config { + value: integer | undefined + }`, + `class MyClass { + filter: QueryContainer | QueryContainer[] + }`, + `interface MyInterface { + input: string | string[] + }`, + `class MyClass { + items: string[] | string + }`, + `interface Config { + nodes: integer[] | integer + }`, + ], + invalid: [ + { + code: `export class Request { + value: string | number | boolean + }`, + errors: [{ messageId: 'noInlineUnion' }] + }, + { + code: `class Container { + content: Success | Error | Pending + }`, + errors: [{ messageId: 'noInlineUnion' }] + }, + { + code: `interface Config { + seed?: long | string + }`, + errors: [{ messageId: 'noInlineUnion' }] + }, + ], +}) + diff --git a/validator/test/prefer-tagged-variants.test.js b/validator/test/prefer-tagged-variants.test.js new file mode 100644 index 0000000000..f794bc6a80 --- /dev/null +++ b/validator/test/prefer-tagged-variants.test.js @@ -0,0 +1,101 @@ +/* + * 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/prefer-tagged-variants.js' + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts*'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, +}) + +ruleTester.run('prefer-tagged-variants', rule, { + valid: [ + `export type QueryType = BoolQuery | TermQuery | RangeQuery`, + `class MyClass { + seed: long | string + }`, + `interface Config { + value: string | number + }`, + `class Container { + query: BoolQuery + }`, + `class MyClass { + items: QueryContainer[] + }`, + `type QueryOrArray = QueryContainer | QueryContainer[] + class MyClass { + filter: QueryOrArray + }`, + `class MyClass { + config: SamplingConfiguration | null + }`, + `interface Status { + state: 'active' | 'inactive' | 'pending' + }`, + `class SearchRequest { + knn?: KnnSearch | KnnSearch[] + }`, + `class EqlRequest { + filter?: QueryContainer[] | QueryContainer + }`, + `interface MyInterface { + data: MyClass[] | MyClass + }`, + ], + invalid: [ + { + code: `class Container { + query: BoolQuery | TermQuery | RangeQuery + }`, + errors: [{ messageId: 'preferTaggedVariants' }] + }, + { + code: `interface Response { + result: SuccessResult | ErrorResult + }`, + errors: [{ messageId: 'preferTaggedVariants' }] + }, + { + code: `class MyClass { + content: TextContent | ImageContent | VideoContent + }`, + errors: [{ messageId: 'preferTaggedVariants' }] + }, + { + code: `interface Config { + processor?: SetProcessor | RemoveProcessor | AppendProcessor + }`, + errors: [{ messageId: 'preferTaggedVariants' }] + }, + { + code: `export class SearchRequest { + query: MatchQuery | TermQuery | BoolQuery | RangeQuery + }`, + errors: [{ messageId: 'preferTaggedVariants' }] + }, + ], +}) +