eslint-plugin-ts-data-forge 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin.d.mts +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +13 -1
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +30 -16
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -1
- package/package.json +6 -6
- package/src/rules/no-unnecessary-type-guard.mts +65 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +94 -0
- package/src/rules/prefer-is-record-and-has-key.mts +84 -20
- package/src/rules/prefer-is-record-and-has-key.test.mts +74 -0
package/dist/plugin.d.mts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const eslintPluginTsDataForge: {
|
|
|
12
12
|
readonly 'prefer-as-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useBrandedNumberCastFunction", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
13
13
|
readonly 'prefer-is-non-null-object': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNonNullObject", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
14
14
|
readonly 'prefer-range-for-loop': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useRangeForLoop", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
15
|
-
readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
15
|
+
readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useHasKey" | "useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
16
16
|
readonly 'prefer-num-safe-parse-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseInt", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
17
17
|
readonly 'prefer-num-safe-parse-float': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseFloat", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
18
18
|
readonly 'no-unnecessary-type-guard': import("@typescript-eslint/utils/ts-eslint").RuleModule<"alwaysFalse" | "alwaysTrue" | "replaceTypeGuard", readonly [(Readonly<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unnecessary-type-guard.d.mts","sourceRoot":"","sources":["../../src/rules/no-unnecessary-type-guard.mts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAUlC,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,+DAA+D;QAC/D,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KAC5B,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"no-unnecessary-type-guard.d.mts","sourceRoot":"","sources":["../../src/rules/no-unnecessary-type-guard.mts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAUlC,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,+DAA+D;QAC/D,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KAC5B,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,kBAAkB,CAAC;AA2FpE,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAkShE,CAAC"}
|
|
@@ -37,7 +37,8 @@ const GUARD_SPECS = {
|
|
|
37
37
|
atoms: ["null", "undefined"],
|
|
38
38
|
replacements: NULLISH_REPLACEMENTS_EXCLUDE
|
|
39
39
|
},
|
|
40
|
-
isNonEmptyString: { kind: "nonEmptyString" }
|
|
40
|
+
isNonEmptyString: { kind: "nonEmptyString" },
|
|
41
|
+
isRecord: { kind: "record" }
|
|
41
42
|
};
|
|
42
43
|
const DEFERRED_OR_OPAQUE_FLAGS = ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never | ts.TypeFlags.TypeParameter | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional | ts.TypeFlags.Substitution;
|
|
43
44
|
const noUnnecessaryTypeGuard = {
|
|
@@ -182,6 +183,14 @@ const noUnnecessaryTypeGuard = {
|
|
|
182
183
|
}
|
|
183
184
|
break;
|
|
184
185
|
}
|
|
186
|
+
case "record": {
|
|
187
|
+
if (parts.every((p) => isGuaranteedRecord(p, checker))) {
|
|
188
|
+
reportConstant(node, canonicalName, true, argument);
|
|
189
|
+
} else if (parts.every((p) => isGuaranteedNotRecord(p, checker))) {
|
|
190
|
+
reportConstant(node, canonicalName, false, argument);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
185
194
|
case "nonEmptyString": {
|
|
186
195
|
const hasNullish = inputAtoms.has("undefined") || inputAtoms.has("null");
|
|
187
196
|
const nonNullishParts = parts.filter((p) => {
|
|
@@ -223,6 +232,9 @@ const collectUnionParts = (type) => {
|
|
|
223
232
|
}
|
|
224
233
|
return parts;
|
|
225
234
|
};
|
|
235
|
+
const isGuaranteedRecord = (type, checker) => checker.getIndexInfoOfType(type, ts.IndexKind.String) !== void 0 && !isCallableType(type, checker) && !checker.isArrayType(type) && !checker.isTupleType(type);
|
|
236
|
+
const isGuaranteedNotRecord = (type, checker) => classifyAtom(type) !== "other" || checker.isArrayType(type) || checker.isTupleType(type) || isCallableType(type, checker);
|
|
237
|
+
const isCallableType = (type, checker) => checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 || checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;
|
|
226
238
|
const classifyAtom = (type) => {
|
|
227
239
|
const { flags } = type;
|
|
228
240
|
if ((flags & ts.TypeFlags.Undefined) !== 0) return "undefined";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unnecessary-type-guard.mjs","sources":["../../src/rules/no-unnecessary-type-guard.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n ESLintUtils,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { type DeepReadonly } from 'ts-type-forge';\nimport * as ts from 'typescript';\nimport {\n buildCalleeResolver,\n buildImportFixes,\n getImportedLocalName,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n /** Names of ts-data-forge guard functions to skip checking. */\n ignore?: readonly string[];\n }>?,\n];\n\ntype MessageIds = 'alwaysTrue' | 'alwaysFalse' | 'replaceTypeGuard';\n\n/**\n * The primitive categories a (non-`null`/`undefined`) union member can fall\n * into. `other` covers anything the rule does not reason about (objects,\n * functions, `void`, …).\n */\ntype AtomKey =\n 'undefined' | 'null' | 'boolean' | 'number' | 'string' | 'bigint' | 'symbol';\n\ntype Atom = AtomKey | 'other';\n\ntype GuardSpec = DeepReadonly<\n | {\n kind: 'narrowTo';\n atoms: AtomKey[];\n replacements?: Record<string, string>;\n }\n | {\n kind: 'excludeFrom';\n atoms: AtomKey[];\n replacements?: Record<string, string>;\n }\n | { kind: 'nonEmptyString' }\n>;\n\n/**\n * Internal marker property attached to every `ts-type-forge` branded type. Its\n * presence (together with the `MinLength` brand key) lets us recognize a\n * `NonEmptyString` without holding a reference to the type itself.\n */\nconst BRAND_MARKER_PROPERTY =\n 'TSTypeForgeInternals--edd2f9ce-7ca5-45b0-9d1a-bd61b9b5d9c3';\n\n/**\n * Brand key carried by `MinLengthString<N>` — and therefore by\n * `NonEmptyString = MinLengthString<1>`. Its value is `MinLengthTuple<N, 0>`, a\n * tuple of `0`s with at least `N` elements, so a required first element means\n * `N >= 1` (the string cannot be empty).\n */\nconst MIN_LENGTH_BRAND_KEY = 'MinLength';\n\nconst NULLISH_REPLACEMENTS_EXCLUDE: Readonly<Record<string, string>> = {\n undefined: 'isNotUndefined',\n null: 'isNotNull',\n} as const;\n\nconst NULLISH_REPLACEMENTS_NARROW: Readonly<Record<string, string>> = {\n undefined: 'isUndefined',\n null: 'isNull',\n} as const;\n\nconst GUARD_SPECS: DeepReadonly<Record<string, GuardSpec>> = {\n isUndefined: { kind: 'narrowTo', atoms: ['undefined'] },\n isNotUndefined: { kind: 'excludeFrom', atoms: ['undefined'] },\n isNull: { kind: 'narrowTo', atoms: ['null'] },\n isNotNull: { kind: 'excludeFrom', atoms: ['null'] },\n isBoolean: { kind: 'narrowTo', atoms: ['boolean'] },\n isNotBoolean: { kind: 'excludeFrom', atoms: ['boolean'] },\n isNumber: { kind: 'narrowTo', atoms: ['number'] },\n isNotNumber: { kind: 'excludeFrom', atoms: ['number'] },\n isString: { kind: 'narrowTo', atoms: ['string'] },\n isNotString: { kind: 'excludeFrom', atoms: ['string'] },\n isBigint: { kind: 'narrowTo', atoms: ['bigint'] },\n isNotBigint: { kind: 'excludeFrom', atoms: ['bigint'] },\n isSymbol: { kind: 'narrowTo', atoms: ['symbol'] },\n isNotSymbol: { kind: 'excludeFrom', atoms: ['symbol'] },\n isNullish: {\n kind: 'narrowTo',\n atoms: ['null', 'undefined'],\n replacements: NULLISH_REPLACEMENTS_NARROW,\n },\n isNonNullish: {\n kind: 'excludeFrom',\n atoms: ['null', 'undefined'],\n replacements: NULLISH_REPLACEMENTS_EXCLUDE,\n },\n isNonEmptyString: { kind: 'nonEmptyString' },\n} as const;\n\nconst DEFERRED_OR_OPAQUE_FLAGS =\n ts.TypeFlags.Any |\n ts.TypeFlags.Unknown |\n ts.TypeFlags.Never |\n ts.TypeFlags.TypeParameter |\n ts.TypeFlags.IndexedAccess |\n ts.TypeFlags.Conditional |\n ts.TypeFlags.Substitution;\n\nexport const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Detect ts-data-forge type guard calls that perform no narrowing (the argument type already satisfies, or can never satisfy, the guard).',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n ignore: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Names of ts-data-forge guard functions to skip checking.',\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n alwaysTrue:\n 'Unnecessary `{{name}}` call: the argument type guarantees it always returns `true`. Replace the call with `true`.',\n alwaysFalse:\n 'Unnecessary `{{name}}` call: the argument type guarantees it always returns `false`. Replace the call with `false`.',\n replaceTypeGuard:\n 'Unnecessary `{{from}}` call: the argument can only be narrowed by `{{to}}`. Use `{{to}}` instead.',\n },\n },\n\n create: (context) => {\n const options = context.options[0];\n\n const parserServices = ESLintUtils.getParserServices(context);\n\n const compilerOptions = parserServices.program.getCompilerOptions();\n\n // Without `strictNullChecks`, `null` / `undefined` are erased from the type\n // system, so reasoning about nullish narrowing is unsound. Disable entirely.\n const strictNullChecks =\n compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false;\n\n if (!strictNullChecks) return {};\n\n const checker = parserServices.program.getTypeChecker();\n\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const ignored = new Set(options?.ignore);\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const resolveGuard = buildCalleeResolver(tsDataForgeImport);\n\n const buildReplaceFix =\n (\n isNamespace: boolean,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n propertyNode: TSESTree.Node,\n replacement: string,\n ): TSESLint.ReportFixFunction =>\n (fixer) => {\n // A namespace member access (`tf.isX`) exposes every export, so the\n // canonical name is always in scope.\n if (isNamespace) {\n return [fixer.replaceText(propertyNode, replacement)];\n }\n\n // If the replacement guard is already imported (possibly under an\n // alias), rename to its in-scope local name. Otherwise add a new\n // import and rename to the canonical name.\n const localName = getImportedLocalName(\n tsDataForgeImport,\n replacement,\n );\n\n if (localName !== undefined) {\n return [fixer.replaceText(propertyNode, localName)];\n }\n\n return [\n ...buildImportFixes(fixer, program, tsDataForgeImport, [\n replacement,\n ]),\n fixer.replaceText(propertyNode, replacement),\n ];\n };\n\n const reportConstant = (\n node: DeepReadonly<TSESTree.CallExpression>,\n name: string,\n value: boolean,\n argument: DeepReadonly<TSESTree.Expression>,\n ): void => {\n // Replacing the call with a literal drops the argument expression, so only\n // autofix when evaluating it cannot have observable side effects.\n const canFix = isSideEffectFreeArg(argument);\n\n context.report({\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node: node as TSESTree.Node,\n messageId: value ? 'alwaysTrue' : 'alwaysFalse',\n data: { name },\n fix: canFix\n ? (fixer) =>\n fixer.replaceText(\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node as TSESTree.Node,\n value ? 'true' : 'false',\n )\n : undefined,\n });\n };\n\n const reportReplace = (\n node: DeepReadonly<TSESTree.CallExpression>,\n from: string,\n to: string,\n isNamespace: boolean,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n propertyNode: TSESTree.Node,\n ): void => {\n context.report({\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node: node as TSESTree.Node,\n messageId: 'replaceTypeGuard',\n data: { from, to },\n fix: buildReplaceFix(isNamespace, propertyNode, to),\n });\n };\n\n return {\n CallExpression: (node) => {\n if (node.arguments.length !== 1) return;\n\n const argument = node.arguments[0];\n\n if (\n argument === undefined ||\n argument.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n const resolved = resolveGuard(node.callee);\n\n if (resolved === undefined) return;\n\n const { canonicalName, propertyNode, isNamespace } = resolved;\n\n if (ignored.has(canonicalName)) return;\n\n const spec = GUARD_SPECS[canonicalName];\n\n if (spec === undefined) return;\n\n const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(argument);\n\n const argType = checker.getTypeAtLocation(argTsNode);\n\n const parts = collectUnionParts(argType);\n\n if (parts === undefined) return; // opaque / generic → bail conservatively\n\n const inputAtoms = new Set(parts.map(classifyAtom));\n\n const hasMemberOutsideTarget = (\n atoms: readonly AtomKey[],\n ): boolean => {\n const targetSet = new Set<Atom>(atoms);\n\n return parts.some((p) => !targetSet.has(classifyAtom(p)));\n };\n\n switch (spec.kind) {\n case 'excludeFrom': {\n const removable = spec.atoms.filter((a) => inputAtoms.has(a));\n\n if (removable.length === 0) {\n // Nothing to exclude → the guard always holds.\n reportConstant(node, canonicalName, true, argument);\n } else if (!hasMemberOutsideTarget(spec.atoms)) {\n // Every member is excluded → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n } else if (removable.length < spec.atoms.length) {\n const replacement = spec.replacements?.[joinAtoms(removable)];\n\n if (replacement !== undefined) {\n reportReplace(\n node,\n canonicalName,\n replacement,\n isNamespace,\n propertyNode,\n );\n }\n }\n\n break;\n }\n\n case 'narrowTo': {\n const present = spec.atoms.filter((a) => inputAtoms.has(a));\n\n if (present.length === 0) {\n // No member is in the target set → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n } else if (!hasMemberOutsideTarget(spec.atoms)) {\n // Input ⊆ target → the guard always holds and narrows nothing.\n reportConstant(node, canonicalName, true, argument);\n } else if (present.length < spec.atoms.length) {\n const replacement = spec.replacements?.[joinAtoms(present)];\n\n if (replacement !== undefined) {\n reportReplace(\n node,\n canonicalName,\n replacement,\n isNamespace,\n propertyNode,\n );\n }\n }\n\n break;\n }\n\n case 'nonEmptyString': {\n const hasNullish =\n inputAtoms.has('undefined') || inputAtoms.has('null');\n\n const nonNullishParts = parts.filter((p) => {\n const atom = classifyAtom(p);\n\n return atom !== 'undefined' && atom !== 'null';\n });\n\n if (!nonNullishParts.some(couldBeNonEmptyString)) {\n // No member can be a non-empty string → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n\n break;\n }\n\n if (\n nonNullishParts.some(\n (p) => !isGuaranteedNonEmptyString(p, checker),\n )\n ) {\n break; // still does real string/empty work\n }\n\n if (hasNullish) {\n reportReplace(\n node,\n canonicalName,\n 'isNonNullish',\n isNamespace,\n propertyNode,\n );\n } else {\n reportConstant(node, canonicalName, true, argument);\n }\n\n break;\n }\n }\n },\n };\n },\n defaultOptions: [{}],\n } as const;\n\n/**\n * Decomposes a type into its union members. Returns `undefined` when any member\n * is opaque (`any` / `unknown` / `never`) or deferred (type parameter, indexed\n * access, …), signalling that the rule should not draw conclusions.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {\n const parts = type.isUnion() ? type.types : ([type] as const);\n\n for (const part of parts) {\n if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0) return undefined;\n }\n\n return parts;\n};\n\n/** Classifies a single (non-union) type into a primitive {@link Atom}. */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst classifyAtom = (type: ts.Type): Atom => {\n const { flags } = type;\n\n if ((flags & ts.TypeFlags.Undefined) !== 0) return 'undefined';\n\n if ((flags & ts.TypeFlags.Null) !== 0) return 'null';\n\n if ((flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean';\n\n if ((flags & ts.TypeFlags.NumberLike) !== 0) return 'number';\n\n if ((flags & ts.TypeFlags.StringLike) !== 0) return 'string';\n\n if ((flags & ts.TypeFlags.BigIntLike) !== 0) return 'bigint';\n\n if ((flags & ts.TypeFlags.ESSymbolLike) !== 0) return 'symbol';\n\n // Branded primitives (e.g. `NonEmptyString = string & {...}`) are\n // intersections; classify them by their underlying primitive constituent.\n if (type.isIntersection()) {\n for (const constituent of type.types) {\n const atom = classifyAtom(constituent);\n\n if (atom !== 'other') return atom;\n }\n }\n\n return 'other';\n};\n\nconst ATOM_ORDER: readonly AtomKey[] = [\n 'undefined',\n 'null',\n 'boolean',\n 'number',\n 'string',\n 'bigint',\n 'symbol',\n] as const;\n\n/** A stable key for a set of atoms, used to look up replacement guards. */\nconst joinAtoms = (atoms: readonly AtomKey[]): string =>\n ATOM_ORDER.filter((atom) => atoms.includes(atom)).join('|');\n\n/**\n * Returns `true` when the type is guaranteed to be a non-empty string: either a\n * non-empty string literal or a value branded as `NonEmptyString`\n * (`MinLengthString<N>` with `N >= 1`).\n */\nconst isGuaranteedNonEmptyString = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n): boolean => {\n if (type.isStringLiteral()) return type.value !== '';\n\n if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined) return false;\n\n const minLengthSymbol = type.getProperty(MIN_LENGTH_BRAND_KEY);\n\n if (minLengthSymbol === undefined) return false;\n\n // The `MinLength` brand value is `MinLengthTuple<N, 0>`; it has a required\n // first element (`'0'`) exactly when `N >= 1`, i.e. the string is non-empty.\n // `MinLengthString<0>` brands with a plain array and has no such element.\n return (\n checker.getTypeOfSymbol(minLengthSymbol).getProperty('0') !== undefined\n );\n};\n\n/**\n * Returns `true` when the type can possibly be a non-empty string (a general\n * `string`, a branded string, or a non-empty string literal). The empty string\n * literal and non-string types cannot.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst couldBeNonEmptyString = (type: ts.Type): boolean => {\n if (type.isStringLiteral()) return type.value !== '';\n\n return classifyAtom(type) === 'string';\n};\n\n/**\n * Returns `true` when evaluating the expression cannot have observable side\n * effects, so replacing the surrounding call with a literal is safe. Limited to\n * simple references (identifiers, `this`, literals, and member-access chains).\n */\n\nconst isSideEffectFreeArg = (node: DeepReadonly<TSESTree.Node>): boolean => {\n // `node.type` is one of 130+ AST kinds; only a handful are relevant here.\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check\n switch (node.type) {\n case AST_NODE_TYPES.Identifier:\n case AST_NODE_TYPES.ThisExpression:\n case AST_NODE_TYPES.Literal:\n return true;\n\n case AST_NODE_TYPES.MemberExpression:\n return (\n isSideEffectFreeArg(node.object) &&\n (!node.computed || isSideEffectFreeArg(node.property))\n );\n\n default:\n return false;\n }\n};\n"],"names":[],"mappings":";;;;AAqDA,MAAM,qBAAA,GACJ,4DAAA;AAQF,MAAM,oBAAA,GAAuB,WAAA;AAE7B,MAAM,4BAAA,GAAiE;AAAA,EACrE,SAAA,EAAW,gBAAA;AAAA,EACX,IAAA,EAAM;AACR,CAAA;AAEA,MAAM,2BAAA,GAAgE;AAAA,EACpE,SAAA,EAAW,aAAA;AAAA,EACX,IAAA,EAAM;AACR,CAAA;AAEA,MAAM,WAAA,GAAuD;AAAA,EAC3D,aAAa,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,WAAW,CAAA,EAAE;AAAA,EACtD,gBAAgB,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,WAAW,CAAA,EAAE;AAAA,EAC5D,QAAQ,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,MAAM,CAAA,EAAE;AAAA,EAC5C,WAAW,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,MAAM,CAAA,EAAE;AAAA,EAClD,WAAW,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,SAAS,CAAA,EAAE;AAAA,EAClD,cAAc,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,SAAS,CAAA,EAAE;AAAA,EACxD,UAAU,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EAChD,aAAa,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EACtD,UAAU,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EAChD,aAAa,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EACtD,UAAU,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EAChD,aAAa,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EACtD,UAAU,EAAE,IAAA,EAAM,YAAY,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EAChD,aAAa,EAAE,IAAA,EAAM,eAAe,KAAA,EAAO,CAAC,QAAQ,CAAA,EAAE;AAAA,EACtD,SAAA,EAAW;AAAA,IACT,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,CAAC,MAAA,EAAQ,WAAW,CAAA;AAAA,IAC3B,YAAA,EAAc;AAAA,GAChB;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,IAAA,EAAM,aAAA;AAAA,IACN,KAAA,EAAO,CAAC,MAAA,EAAQ,WAAW,CAAA;AAAA,IAC3B,YAAA,EAAc;AAAA,GAChB;AAAA,EACA,gBAAA,EAAkB,EAAE,IAAA,EAAM,gBAAA;AAC5B,CAAA;AAEA,MAAM,wBAAA,GACJ,GAAG,SAAA,CAAU,GAAA,GACb,GAAG,SAAA,CAAU,OAAA,GACb,GAAG,SAAA,CAAU,KAAA,GACb,GAAG,SAAA,CAAU,aAAA,GACb,GAAG,SAAA,CAAU,aAAA,GACb,GAAG,SAAA,CAAU,WAAA,GACb,GAAG,SAAA,CAAU,YAAA;AAER,MAAM,sBAAA,GACX;AAAA,EACE,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,MAAA,EAAQ;AAAA,YACN,IAAA,EAAM,OAAA;AAAA,YACN,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,YACxB,WAAA,EACE;AAAA;AACJ,SACF;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,UAAA,EACE,mHAAA;AAAA,MACF,WAAA,EACE,qHAAA;AAAA,MACF,gBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA;AAEjC,IAAA,MAAM,cAAA,GAAiB,WAAA,CAAY,iBAAA,CAAkB,OAAO,CAAA;AAE5D,IAAA,MAAM,eAAA,GAAkB,cAAA,CAAe,OAAA,CAAQ,kBAAA,EAAmB;AAIlE,IAAA,MAAM,gBAAA,GACJ,eAAA,CAAgB,gBAAA,IAAoB,eAAA,CAAgB,MAAA,IAAU,KAAA;AAEhE,IAAA,IAAI,CAAC,gBAAA,EAAkB,OAAO,EAAC;AAE/B,IAAA,MAAM,OAAA,GAAU,cAAA,CAAe,OAAA,CAAQ,cAAA,EAAe;AAEtD,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,IAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAI,OAAA,EAAS,MAAM,CAAA;AAEvC,IAAA,MAAM,iBAAA,GAAoB,qBAAqB,OAAO,CAAA;AAEtD,IAAA,MAAM,YAAA,GAAe,oBAAoB,iBAAiB,CAAA;AAE1D,IAAA,MAAM,kBACJ,CACE,WAAA,EAEA,YAAA,EACA,WAAA,KAEF,CAAC,KAAA,KAAU;AAGT,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,OAAO,CAAC,KAAA,CAAM,WAAA,CAAY,YAAA,EAAc,WAAW,CAAC,CAAA;AAAA,MACtD;AAKA,MAAA,MAAM,SAAA,GAAY,oBAAA;AAAA,QAChB,iBAAA;AAAA,QACA;AAAA,OACF;AAEA,MAAA,IAAI,cAAc,MAAA,EAAW;AAC3B,QAAA,OAAO,CAAC,KAAA,CAAM,WAAA,CAAY,YAAA,EAAc,SAAS,CAAC,CAAA;AAAA,MACpD;AAEA,MAAA,OAAO;AAAA,QACL,GAAG,gBAAA,CAAiB,KAAA,EAAO,OAAA,EAAS,iBAAA,EAAmB;AAAA,UACrD;AAAA,SACD,CAAA;AAAA,QACD,KAAA,CAAM,WAAA,CAAY,YAAA,EAAc,WAAW;AAAA,OAC7C;AAAA,IACF,CAAA;AAEF,IAAA,MAAM,cAAA,GAAiB,CACrB,IAAA,EACA,IAAA,EACA,OACA,QAAA,KACS;AAGT,MAAA,MAAM,MAAA,GAAS,oBAAoB,QAAQ,CAAA;AAE3C,MAAA,OAAA,CAAQ,MAAA,CAAO;AAAA;AAAA,QAEb,IAAA;AAAA,QACA,SAAA,EAAW,QAAQ,YAAA,GAAe,aAAA;AAAA,QAClC,IAAA,EAAM,EAAE,IAAA,EAAK;AAAA,QACb,GAAA,EAAK,MAAA,GACD,CAAC,KAAA,KACC,KAAA,CAAM,WAAA;AAAA;AAAA,UAEJ,IAAA;AAAA,UACA,QAAQ,MAAA,GAAS;AAAA,SACnB,GACF;AAAA,OACL,CAAA;AAAA,IACH,CAAA;AAEA,IAAA,MAAM,gBAAgB,CACpB,IAAA,EACA,IAAA,EACA,EAAA,EACA,aAEA,YAAA,KACS;AACT,MAAA,OAAA,CAAQ,MAAA,CAAO;AAAA;AAAA,QAEb,IAAA;AAAA,QACA,SAAA,EAAW,kBAAA;AAAA,QACX,IAAA,EAAM,EAAE,IAAA,EAAM,EAAA,EAAG;AAAA,QACjB,GAAA,EAAK,eAAA,CAAgB,WAAA,EAAa,YAAA,EAAc,EAAE;AAAA,OACnD,CAAA;AAAA,IACH,CAAA;AAEA,IAAA,OAAO;AAAA,MACL,cAAA,EAAgB,CAAC,IAAA,KAAS;AACxB,QAAA,IAAI,IAAA,CAAK,SAAA,CAAU,MAAA,KAAW,CAAA,EAAG;AAEjC,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAEjC,QAAA,IACE,QAAA,KAAa,MAAA,IACb,QAAA,CAAS,IAAA,KAAS,eAAe,aAAA,EACjC;AACA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,QAAA,GAAW,YAAA,CAAa,IAAA,CAAK,MAAM,CAAA;AAEzC,QAAA,IAAI,aAAa,MAAA,EAAW;AAE5B,QAAA,MAAM,EAAE,aAAA,EAAe,YAAA,EAAc,WAAA,EAAY,GAAI,QAAA;AAErD,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA,EAAG;AAEhC,QAAA,MAAM,IAAA,GAAO,YAAY,aAAa,CAAA;AAEtC,QAAA,IAAI,SAAS,MAAA,EAAW;AAExB,QAAA,MAAM,SAAA,GAAY,cAAA,CAAe,qBAAA,CAAsB,GAAA,CAAI,QAAQ,CAAA;AAEnE,QAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,iBAAA,CAAkB,SAAS,CAAA;AAEnD,QAAA,MAAM,KAAA,GAAQ,kBAAkB,OAAO,CAAA;AAEvC,QAAA,IAAI,UAAU,MAAA,EAAW;AAEzB,QAAA,MAAM,aAAa,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,YAAY,CAAC,CAAA;AAElD,QAAA,MAAM,sBAAA,GAAyB,CAC7B,KAAA,KACY;AACZ,UAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAU,KAAK,CAAA;AAErC,UAAA,OAAO,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAC,UAAU,GAAA,CAAI,YAAA,CAAa,CAAC,CAAC,CAAC,CAAA;AAAA,QAC1D,CAAA;AAEA,QAAA,QAAQ,KAAK,IAAA;AAAM,UACjB,KAAK,aAAA,EAAe;AAClB,YAAA,MAAM,SAAA,GAAY,KAAK,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,CAAC,CAAA;AAE5D,YAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAE1B,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,QAAQ,CAAA;AAAA,YACpD,CAAA,MAAA,IAAW,CAAC,sBAAA,CAAuB,IAAA,CAAK,KAAK,CAAA,EAAG;AAE9C,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,KAAA,EAAO,QAAQ,CAAA;AAAA,YACrD,CAAA,MAAA,IAAW,SAAA,CAAU,MAAA,GAAS,IAAA,CAAK,MAAM,MAAA,EAAQ;AAC/C,cAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,GAAe,SAAA,CAAU,SAAS,CAAC,CAAA;AAE5D,cAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,gBAAA,aAAA;AAAA,kBACE,IAAA;AAAA,kBACA,aAAA;AAAA,kBACA,WAAA;AAAA,kBACA,WAAA;AAAA,kBACA;AAAA,iBACF;AAAA,cACF;AAAA,YACF;AAEA,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,UAAA,EAAY;AACf,YAAA,MAAM,OAAA,GAAU,KAAK,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,CAAC,CAAA;AAE1D,YAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAExB,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,KAAA,EAAO,QAAQ,CAAA;AAAA,YACrD,CAAA,MAAA,IAAW,CAAC,sBAAA,CAAuB,IAAA,CAAK,KAAK,CAAA,EAAG;AAE9C,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,QAAQ,CAAA;AAAA,YACpD,CAAA,MAAA,IAAW,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,MAAM,MAAA,EAAQ;AAC7C,cAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,GAAe,SAAA,CAAU,OAAO,CAAC,CAAA;AAE1D,cAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,gBAAA,aAAA;AAAA,kBACE,IAAA;AAAA,kBACA,aAAA;AAAA,kBACA,WAAA;AAAA,kBACA,WAAA;AAAA,kBACA;AAAA,iBACF;AAAA,cACF;AAAA,YACF;AAEA,YAAA;AAAA,UACF;AAAA,UAEA,KAAK,gBAAA,EAAkB;AACrB,YAAA,MAAM,aACJ,UAAA,CAAW,GAAA,CAAI,WAAW,CAAA,IAAK,UAAA,CAAW,IAAI,MAAM,CAAA;AAEtD,YAAA,MAAM,eAAA,GAAkB,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM;AAC1C,cAAA,MAAM,IAAA,GAAO,aAAa,CAAC,CAAA;AAE3B,cAAA,OAAO,IAAA,KAAS,eAAe,IAAA,KAAS,MAAA;AAAA,YAC1C,CAAC,CAAA;AAED,YAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,qBAAqB,CAAA,EAAG;AAEhD,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,KAAA,EAAO,QAAQ,CAAA;AAEnD,cAAA;AAAA,YACF;AAEA,YAAA,IACE,eAAA,CAAgB,IAAA;AAAA,cACd,CAAC,CAAA,KAAM,CAAC,0BAAA,CAA2B,GAAG,OAAO;AAAA,aAC/C,EACA;AACA,cAAA;AAAA,YACF;AAEA,YAAA,IAAI,UAAA,EAAY;AACd,cAAA,aAAA;AAAA,gBACE,IAAA;AAAA,gBACA,aAAA;AAAA,gBACA,cAAA;AAAA,gBACA,WAAA;AAAA,gBACA;AAAA,eACF;AAAA,YACF,CAAA,MAAO;AACL,cAAA,cAAA,CAAe,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,QAAQ,CAAA;AAAA,YACpD;AAEA,YAAA;AAAA,UACF;AAAA;AACF,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE;AACrB;AAQF,MAAM,iBAAA,GAAoB,CAAC,IAAA,KAAkD;AAC3E,EAAA,MAAM,QAAQ,IAAA,CAAK,OAAA,KAAY,IAAA,CAAK,KAAA,GAAS,CAAC,IAAI,CAAA;AAElD,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAA,CAAK,IAAA,CAAK,KAAA,GAAQ,wBAAA,MAA8B,CAAA,EAAG,OAAO,MAAA;AAAA,EAC5D;AAEA,EAAA,OAAO,KAAA;AACT,CAAA;AAIA,MAAM,YAAA,GAAe,CAAC,IAAA,KAAwB;AAC5C,EAAA,MAAM,EAAE,OAAM,GAAI,IAAA;AAElB,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,SAAA,MAAe,GAAG,OAAO,WAAA;AAEnD,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,IAAA,MAAU,GAAG,OAAO,MAAA;AAE9C,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,WAAA,MAAiB,GAAG,OAAO,SAAA;AAErD,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,UAAA,MAAgB,GAAG,OAAO,QAAA;AAEpD,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,UAAA,MAAgB,GAAG,OAAO,QAAA;AAEpD,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,UAAA,MAAgB,GAAG,OAAO,QAAA;AAEpD,EAAA,IAAA,CAAK,KAAA,GAAQ,EAAA,CAAG,SAAA,CAAU,YAAA,MAAkB,GAAG,OAAO,QAAA;AAItD,EAAA,IAAI,IAAA,CAAK,gBAAe,EAAG;AACzB,IAAA,KAAA,MAAW,WAAA,IAAe,KAAK,KAAA,EAAO;AACpC,MAAA,MAAM,IAAA,GAAO,aAAa,WAAW,CAAA;AAErC,MAAA,IAAI,IAAA,KAAS,SAAS,OAAO,IAAA;AAAA,IAC/B;AAAA,EACF;AAEA,EAAA,OAAO,OAAA;AACT,CAAA;AAEA,MAAM,UAAA,GAAiC;AAAA,EACrC,WAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA;AAGA,MAAM,SAAA,GAAY,CAAC,KAAA,KACjB,UAAA,CAAW,MAAA,CAAO,CAAC,IAAA,KAAS,KAAA,CAAM,QAAA,CAAS,IAAI,CAAC,CAAA,CAAE,KAAK,GAAG,CAAA;AAO5D,MAAM,0BAAA,GAA6B,CAEjC,IAAA,EAEA,OAAA,KACY;AACZ,EAAA,IAAI,IAAA,CAAK,eAAA,EAAgB,EAAG,OAAO,KAAK,KAAA,KAAU,EAAA;AAElD,EAAA,IAAI,IAAA,CAAK,WAAA,CAAY,qBAAqB,CAAA,KAAM,QAAW,OAAO,KAAA;AAElE,EAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,WAAA,CAAY,oBAAoB,CAAA;AAE7D,EAAA,IAAI,eAAA,KAAoB,QAAW,OAAO,KAAA;AAK1C,EAAA,OACE,QAAQ,eAAA,CAAgB,eAAe,CAAA,CAAE,WAAA,CAAY,GAAG,CAAA,KAAM,MAAA;AAElE,CAAA;AAQA,MAAM,qBAAA,GAAwB,CAAC,IAAA,KAA2B;AACxD,EAAA,IAAI,IAAA,CAAK,eAAA,EAAgB,EAAG,OAAO,KAAK,KAAA,KAAU,EAAA;AAElD,EAAA,OAAO,YAAA,CAAa,IAAI,CAAA,KAAM,QAAA;AAChC,CAAA;AAQA,MAAM,mBAAA,GAAsB,CAAC,IAAA,KAA+C;AAG1E,EAAA,QAAQ,KAAK,IAAA;AAAM,IACjB,KAAK,cAAA,CAAe,UAAA;AAAA,IACpB,KAAK,cAAA,CAAe,cAAA;AAAA,IACpB,KAAK,cAAA,CAAe,OAAA;AAClB,MAAA,OAAO,IAAA;AAAA,IAET,KAAK,cAAA,CAAe,gBAAA;AAClB,MAAA,OACE,mBAAA,CAAoB,KAAK,MAAM,CAAA,KAC9B,CAAC,IAAA,CAAK,QAAA,IAAY,mBAAA,CAAoB,IAAA,CAAK,QAAQ,CAAA,CAAA;AAAA,IAGxD;AACE,MAAA,OAAO,KAAA;AAAA;AAEb,CAAA;;;;"}
|
|
1
|
+
{"version":3,"file":"no-unnecessary-type-guard.mjs","sources":["../../src/rules/no-unnecessary-type-guard.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n ESLintUtils,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { type DeepReadonly } from 'ts-type-forge';\nimport * as ts from 'typescript';\nimport {\n buildCalleeResolver,\n buildImportFixes,\n getImportedLocalName,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n /** Names of ts-data-forge guard functions to skip checking. */\n ignore?: readonly string[];\n }>?,\n];\n\ntype MessageIds = 'alwaysTrue' | 'alwaysFalse' | 'replaceTypeGuard';\n\n/**\n * The primitive categories a (non-`null`/`undefined`) union member can fall\n * into. `other` covers anything the rule does not reason about (objects,\n * functions, `void`, …).\n */\ntype AtomKey =\n 'undefined' | 'null' | 'boolean' | 'number' | 'string' | 'bigint' | 'symbol';\n\ntype Atom = AtomKey | 'other';\n\ntype GuardSpec = DeepReadonly<\n | {\n kind: 'narrowTo';\n atoms: AtomKey[];\n replacements?: Record<string, string>;\n }\n | {\n kind: 'excludeFrom';\n atoms: AtomKey[];\n replacements?: Record<string, string>;\n }\n | { kind: 'nonEmptyString' }\n | { kind: 'record' }\n>;\n\n/**\n * Internal marker property attached to every `ts-type-forge` branded type. Its\n * presence (together with the `MinLength` brand key) lets us recognize a\n * `NonEmptyString` without holding a reference to the type itself.\n */\nconst BRAND_MARKER_PROPERTY =\n 'TSTypeForgeInternals--edd2f9ce-7ca5-45b0-9d1a-bd61b9b5d9c3';\n\n/**\n * Brand key carried by `MinLengthString<N>` — and therefore by\n * `NonEmptyString = MinLengthString<1>`. Its value is `MinLengthTuple<N, 0>`, a\n * tuple of `0`s with at least `N` elements, so a required first element means\n * `N >= 1` (the string cannot be empty).\n */\nconst MIN_LENGTH_BRAND_KEY = 'MinLength';\n\nconst NULLISH_REPLACEMENTS_EXCLUDE: Readonly<Record<string, string>> = {\n undefined: 'isNotUndefined',\n null: 'isNotNull',\n} as const;\n\nconst NULLISH_REPLACEMENTS_NARROW: Readonly<Record<string, string>> = {\n undefined: 'isUndefined',\n null: 'isNull',\n} as const;\n\nconst GUARD_SPECS: DeepReadonly<Record<string, GuardSpec>> = {\n isUndefined: { kind: 'narrowTo', atoms: ['undefined'] },\n isNotUndefined: { kind: 'excludeFrom', atoms: ['undefined'] },\n isNull: { kind: 'narrowTo', atoms: ['null'] },\n isNotNull: { kind: 'excludeFrom', atoms: ['null'] },\n isBoolean: { kind: 'narrowTo', atoms: ['boolean'] },\n isNotBoolean: { kind: 'excludeFrom', atoms: ['boolean'] },\n isNumber: { kind: 'narrowTo', atoms: ['number'] },\n isNotNumber: { kind: 'excludeFrom', atoms: ['number'] },\n isString: { kind: 'narrowTo', atoms: ['string'] },\n isNotString: { kind: 'excludeFrom', atoms: ['string'] },\n isBigint: { kind: 'narrowTo', atoms: ['bigint'] },\n isNotBigint: { kind: 'excludeFrom', atoms: ['bigint'] },\n isSymbol: { kind: 'narrowTo', atoms: ['symbol'] },\n isNotSymbol: { kind: 'excludeFrom', atoms: ['symbol'] },\n isNullish: {\n kind: 'narrowTo',\n atoms: ['null', 'undefined'],\n replacements: NULLISH_REPLACEMENTS_NARROW,\n },\n isNonNullish: {\n kind: 'excludeFrom',\n atoms: ['null', 'undefined'],\n replacements: NULLISH_REPLACEMENTS_EXCLUDE,\n },\n isNonEmptyString: { kind: 'nonEmptyString' },\n isRecord: { kind: 'record' },\n} as const;\n\nconst DEFERRED_OR_OPAQUE_FLAGS =\n ts.TypeFlags.Any |\n ts.TypeFlags.Unknown |\n ts.TypeFlags.Never |\n ts.TypeFlags.TypeParameter |\n ts.TypeFlags.IndexedAccess |\n ts.TypeFlags.Conditional |\n ts.TypeFlags.Substitution;\n\nexport const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Detect ts-data-forge type guard calls that perform no narrowing (the argument type already satisfies, or can never satisfy, the guard).',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n ignore: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Names of ts-data-forge guard functions to skip checking.',\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n alwaysTrue:\n 'Unnecessary `{{name}}` call: the argument type guarantees it always returns `true`. Replace the call with `true`.',\n alwaysFalse:\n 'Unnecessary `{{name}}` call: the argument type guarantees it always returns `false`. Replace the call with `false`.',\n replaceTypeGuard:\n 'Unnecessary `{{from}}` call: the argument can only be narrowed by `{{to}}`. Use `{{to}}` instead.',\n },\n },\n\n create: (context) => {\n const options = context.options[0];\n\n const parserServices = ESLintUtils.getParserServices(context);\n\n const compilerOptions = parserServices.program.getCompilerOptions();\n\n // Without `strictNullChecks`, `null` / `undefined` are erased from the type\n // system, so reasoning about nullish narrowing is unsound. Disable entirely.\n const strictNullChecks =\n compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false;\n\n if (!strictNullChecks) return {};\n\n const checker = parserServices.program.getTypeChecker();\n\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const ignored = new Set(options?.ignore);\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const resolveGuard = buildCalleeResolver(tsDataForgeImport);\n\n const buildReplaceFix =\n (\n isNamespace: boolean,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n propertyNode: TSESTree.Node,\n replacement: string,\n ): TSESLint.ReportFixFunction =>\n (fixer) => {\n // A namespace member access (`tf.isX`) exposes every export, so the\n // canonical name is always in scope.\n if (isNamespace) {\n return [fixer.replaceText(propertyNode, replacement)];\n }\n\n // If the replacement guard is already imported (possibly under an\n // alias), rename to its in-scope local name. Otherwise add a new\n // import and rename to the canonical name.\n const localName = getImportedLocalName(\n tsDataForgeImport,\n replacement,\n );\n\n if (localName !== undefined) {\n return [fixer.replaceText(propertyNode, localName)];\n }\n\n return [\n ...buildImportFixes(fixer, program, tsDataForgeImport, [\n replacement,\n ]),\n fixer.replaceText(propertyNode, replacement),\n ];\n };\n\n const reportConstant = (\n node: DeepReadonly<TSESTree.CallExpression>,\n name: string,\n value: boolean,\n argument: DeepReadonly<TSESTree.Expression>,\n ): void => {\n // Replacing the call with a literal drops the argument expression, so only\n // autofix when evaluating it cannot have observable side effects.\n const canFix = isSideEffectFreeArg(argument);\n\n context.report({\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node: node as TSESTree.Node,\n messageId: value ? 'alwaysTrue' : 'alwaysFalse',\n data: { name },\n fix: canFix\n ? (fixer) =>\n fixer.replaceText(\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node as TSESTree.Node,\n value ? 'true' : 'false',\n )\n : undefined,\n });\n };\n\n const reportReplace = (\n node: DeepReadonly<TSESTree.CallExpression>,\n from: string,\n to: string,\n isNamespace: boolean,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n propertyNode: TSESTree.Node,\n ): void => {\n context.report({\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n node: node as TSESTree.Node,\n messageId: 'replaceTypeGuard',\n data: { from, to },\n fix: buildReplaceFix(isNamespace, propertyNode, to),\n });\n };\n\n return {\n CallExpression: (node) => {\n if (node.arguments.length !== 1) return;\n\n const argument = node.arguments[0];\n\n if (\n argument === undefined ||\n argument.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n const resolved = resolveGuard(node.callee);\n\n if (resolved === undefined) return;\n\n const { canonicalName, propertyNode, isNamespace } = resolved;\n\n if (ignored.has(canonicalName)) return;\n\n const spec = GUARD_SPECS[canonicalName];\n\n if (spec === undefined) return;\n\n const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(argument);\n\n const argType = checker.getTypeAtLocation(argTsNode);\n\n const parts = collectUnionParts(argType);\n\n if (parts === undefined) return; // opaque / generic → bail conservatively\n\n const inputAtoms = new Set(parts.map(classifyAtom));\n\n const hasMemberOutsideTarget = (\n atoms: readonly AtomKey[],\n ): boolean => {\n const targetSet = new Set<Atom>(atoms);\n\n return parts.some((p) => !targetSet.has(classifyAtom(p)));\n };\n\n switch (spec.kind) {\n case 'excludeFrom': {\n const removable = spec.atoms.filter((a) => inputAtoms.has(a));\n\n if (removable.length === 0) {\n // Nothing to exclude → the guard always holds.\n reportConstant(node, canonicalName, true, argument);\n } else if (!hasMemberOutsideTarget(spec.atoms)) {\n // Every member is excluded → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n } else if (removable.length < spec.atoms.length) {\n const replacement = spec.replacements?.[joinAtoms(removable)];\n\n if (replacement !== undefined) {\n reportReplace(\n node,\n canonicalName,\n replacement,\n isNamespace,\n propertyNode,\n );\n }\n }\n\n break;\n }\n\n case 'narrowTo': {\n const present = spec.atoms.filter((a) => inputAtoms.has(a));\n\n if (present.length === 0) {\n // No member is in the target set → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n } else if (!hasMemberOutsideTarget(spec.atoms)) {\n // Input ⊆ target → the guard always holds and narrows nothing.\n reportConstant(node, canonicalName, true, argument);\n } else if (present.length < spec.atoms.length) {\n const replacement = spec.replacements?.[joinAtoms(present)];\n\n if (replacement !== undefined) {\n reportReplace(\n node,\n canonicalName,\n replacement,\n isNamespace,\n propertyNode,\n );\n }\n }\n\n break;\n }\n\n case 'record': {\n if (parts.every((p) => isGuaranteedRecord(p, checker))) {\n // Every member already satisfies `UnknownRecord`.\n reportConstant(node, canonicalName, true, argument);\n } else if (\n parts.every((p) => isGuaranteedNotRecord(p, checker))\n ) {\n // No member can ever be a non-null, non-array object.\n reportConstant(node, canonicalName, false, argument);\n }\n\n break;\n }\n\n case 'nonEmptyString': {\n const hasNullish =\n inputAtoms.has('undefined') || inputAtoms.has('null');\n\n const nonNullishParts = parts.filter((p) => {\n const atom = classifyAtom(p);\n\n return atom !== 'undefined' && atom !== 'null';\n });\n\n if (!nonNullishParts.some(couldBeNonEmptyString)) {\n // No member can be a non-empty string → the guard never holds.\n reportConstant(node, canonicalName, false, argument);\n\n break;\n }\n\n if (\n nonNullishParts.some(\n (p) => !isGuaranteedNonEmptyString(p, checker),\n )\n ) {\n break; // still does real string/empty work\n }\n\n if (hasNullish) {\n reportReplace(\n node,\n canonicalName,\n 'isNonNullish',\n isNamespace,\n propertyNode,\n );\n } else {\n reportConstant(node, canonicalName, true, argument);\n }\n\n break;\n }\n }\n },\n };\n },\n defaultOptions: [{}],\n } as const;\n\n/**\n * Decomposes a type into its union members. Returns `undefined` when any member\n * is opaque (`any` / `unknown` / `never`) or deferred (type parameter, indexed\n * access, …), signalling that the rule should not draw conclusions.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {\n const parts = type.isUnion() ? type.types : ([type] as const);\n\n for (const part of parts) {\n if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0) return undefined;\n }\n\n return parts;\n};\n\n/**\n * Whether `isRecord` is guaranteed to accept this (non-union) type.\n *\n * `isRecord` narrows to `UnknownRecord` (`ReadonlyRecord<string, unknown>`), so\n * a string index signature is exactly what it asks for and its value type is\n * `unknown`, which anything satisfies. Arrays and callables are excluded even\n * when they carry one, because `isRecord` rejects them at runtime\n * (`Array.isArray` is true; `typeof fn === 'object'` is false).\n *\n * Deliberately conservative: an object type without an index signature of its\n * own is reported as neither guaranteed-accepted nor guaranteed-rejected, even\n * where TypeScript would accept it through an implicit index signature.\n */\nconst isGuaranteedRecord = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n): boolean =>\n checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&\n !isCallableType(type, checker) &&\n !checker.isArrayType(type) &&\n !checker.isTupleType(type);\n\n/**\n * Whether `isRecord` is guaranteed to reject this (non-union) type: every\n * primitive (`null` included), every array or tuple, and every callable fails\n * its `typeof x === 'object' && x !== null && !Array.isArray(x)` check.\n */\nconst isGuaranteedNotRecord = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n): boolean =>\n classifyAtom(type) !== 'other' ||\n checker.isArrayType(type) ||\n checker.isTupleType(type) ||\n isCallableType(type, checker);\n\nconst isCallableType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n): boolean =>\n checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||\n checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;\n\n/** Classifies a single (non-union) type into a primitive {@link Atom}. */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst classifyAtom = (type: ts.Type): Atom => {\n const { flags } = type;\n\n if ((flags & ts.TypeFlags.Undefined) !== 0) return 'undefined';\n\n if ((flags & ts.TypeFlags.Null) !== 0) return 'null';\n\n if ((flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean';\n\n if ((flags & ts.TypeFlags.NumberLike) !== 0) return 'number';\n\n if ((flags & ts.TypeFlags.StringLike) !== 0) return 'string';\n\n if ((flags & ts.TypeFlags.BigIntLike) !== 0) return 'bigint';\n\n if ((flags & ts.TypeFlags.ESSymbolLike) !== 0) return 'symbol';\n\n // Branded primitives (e.g. `NonEmptyString = string & {...}`) are\n // intersections; classify them by their underlying primitive constituent.\n if (type.isIntersection()) {\n for (const constituent of type.types) {\n const atom = classifyAtom(constituent);\n\n if (atom !== 'other') return atom;\n }\n }\n\n return 'other';\n};\n\nconst ATOM_ORDER: readonly AtomKey[] = [\n 'undefined',\n 'null',\n 'boolean',\n 'number',\n 'string',\n 'bigint',\n 'symbol',\n] as const;\n\n/** A stable key for a set of atoms, used to look up replacement guards. */\nconst joinAtoms = (atoms: readonly AtomKey[]): string =>\n ATOM_ORDER.filter((atom) => atoms.includes(atom)).join('|');\n\n/**\n * Returns `true` when the type is guaranteed to be a non-empty string: either a\n * non-empty string literal or a value branded as `NonEmptyString`\n * (`MinLengthString<N>` with `N >= 1`).\n */\nconst isGuaranteedNonEmptyString = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n): boolean => {\n if (type.isStringLiteral()) return type.value !== '';\n\n if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined) return false;\n\n const minLengthSymbol = type.getProperty(MIN_LENGTH_BRAND_KEY);\n\n if (minLengthSymbol === undefined) return false;\n\n // The `MinLength` brand value is `MinLengthTuple<N, 0>`; it has a required\n // first element (`'0'`) exactly when `N >= 1`, i.e. the string is non-empty.\n // `MinLengthString<0>` brands with a plain array and has no such element.\n return (\n checker.getTypeOfSymbol(minLengthSymbol).getProperty('0') !== undefined\n );\n};\n\n/**\n * Returns `true` when the type can possibly be a non-empty string (a general\n * `string`, a branded string, or a non-empty string literal). The empty string\n * literal and non-string types cannot.\n */\n// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\nconst couldBeNonEmptyString = (type: ts.Type): boolean => {\n if (type.isStringLiteral()) return type.value !== '';\n\n return classifyAtom(type) === 'string';\n};\n\n/**\n * Returns `true` when evaluating the expression cannot have observable side\n * effects, so replacing the surrounding call with a literal is safe. Limited to\n * simple references (identifiers, `this`, literals, and member-access chains).\n */\n\nconst isSideEffectFreeArg = (node: DeepReadonly<TSESTree.Node>): boolean => {\n // `node.type` is one of 130+ AST kinds; only a handful are relevant here.\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check\n switch (node.type) {\n case AST_NODE_TYPES.Identifier:\n case AST_NODE_TYPES.ThisExpression:\n case AST_NODE_TYPES.Literal:\n return true;\n\n case AST_NODE_TYPES.MemberExpression:\n return (\n isSideEffectFreeArg(node.object) &&\n (!node.computed || isSideEffectFreeArg(node.property))\n );\n\n default:\n return false;\n }\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
2
|
type Options = readonly [];
|
|
3
|
-
type MessageIds = 'useIsRecordAndHasKey';
|
|
3
|
+
type MessageIds = 'useIsRecordAndHasKey' | 'useHasKey';
|
|
4
4
|
export declare const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options>;
|
|
5
5
|
export {};
|
|
6
6
|
//# sourceMappingURL=prefer-is-record-and-has-key.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AASlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4KjE,CAAC"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import * as ts from 'typescript';
|
|
2
4
|
import { getTsDataForgeImport, getNamedImports, buildImportFixes } from './import-utils.mjs';
|
|
3
5
|
|
|
4
6
|
const preferIsRecordAndHasKey = {
|
|
@@ -10,13 +12,21 @@ const preferIsRecordAndHasKey = {
|
|
|
10
12
|
fixable: "code",
|
|
11
13
|
schema: [],
|
|
12
14
|
messages: {
|
|
13
|
-
useIsRecordAndHasKey: "Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge."
|
|
15
|
+
useIsRecordAndHasKey: "Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.",
|
|
16
|
+
useHasKey: "Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge."
|
|
14
17
|
}
|
|
15
18
|
},
|
|
16
19
|
create: (context) => {
|
|
17
20
|
const sourceCode = context.sourceCode;
|
|
18
21
|
const program = sourceCode.ast;
|
|
19
22
|
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
23
|
+
const services = sourceCode.parserServices;
|
|
24
|
+
const checker = services?.program?.getTypeChecker();
|
|
25
|
+
const isKnownRecord = (expression) => {
|
|
26
|
+
if (checker === void 0) return false;
|
|
27
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);
|
|
28
|
+
return tsNode !== void 0 && isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode));
|
|
29
|
+
};
|
|
20
30
|
const mut_nodesToFix = [];
|
|
21
31
|
return {
|
|
22
32
|
// Handle Object.hasOwn(obj, key)
|
|
@@ -51,37 +61,38 @@ const preferIsRecordAndHasKey = {
|
|
|
51
61
|
},
|
|
52
62
|
"Program:exit": () => {
|
|
53
63
|
const namedImports = getNamedImports(tsDataForgeImport);
|
|
54
|
-
const
|
|
55
|
-
|
|
64
|
+
const rewrites = mut_nodesToFix.map((entry) => ({
|
|
65
|
+
...entry,
|
|
66
|
+
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
67
|
+
// true, so it is left out of the replacement entirely.
|
|
68
|
+
needsIsRecord: !isKnownRecord(entry.objExpression)
|
|
69
|
+
}));
|
|
70
|
+
const importsToAdd = [
|
|
71
|
+
...rewrites.some(({ needsIsRecord }) => needsIsRecord) && !namedImports.includes("isRecord") ? ["isRecord"] : [],
|
|
72
|
+
...namedImports.includes("hasKey") ? [] : ["hasKey"]
|
|
73
|
+
];
|
|
56
74
|
for (const [
|
|
57
75
|
index,
|
|
58
|
-
{ node, objExpression, keyExpression }
|
|
59
|
-
] of
|
|
76
|
+
{ node, objExpression, keyExpression, needsIsRecord }
|
|
77
|
+
] of rewrites.entries()) {
|
|
60
78
|
const objText = sourceCode.getText(objExpression);
|
|
61
79
|
const keyText = sourceCode.getText(keyExpression);
|
|
62
80
|
const originalText = sourceCode.getText(node);
|
|
63
81
|
context.report({
|
|
64
82
|
node,
|
|
65
|
-
messageId: "useIsRecordAndHasKey",
|
|
83
|
+
messageId: needsIsRecord ? "useIsRecordAndHasKey" : "useHasKey",
|
|
66
84
|
data: {
|
|
67
85
|
original: originalText,
|
|
68
86
|
objName: objText,
|
|
69
87
|
keyName: keyText
|
|
70
88
|
},
|
|
71
89
|
fix: (fixer) => {
|
|
72
|
-
const replacement = `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`;
|
|
73
|
-
const
|
|
74
|
-
if (!hasIsRecordImport) {
|
|
75
|
-
mut_importsToAdd.push("isRecord");
|
|
76
|
-
}
|
|
77
|
-
if (!hasHasKeyImport) {
|
|
78
|
-
mut_importsToAdd.push("hasKey");
|
|
79
|
-
}
|
|
80
|
-
const importFixes = index === 0 && mut_importsToAdd.length > 0 ? buildImportFixes(
|
|
90
|
+
const replacement = needsIsRecord ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))` : `hasKey(${objText}, ${keyText})`;
|
|
91
|
+
const importFixes = index === 0 && importsToAdd.length > 0 ? buildImportFixes(
|
|
81
92
|
fixer,
|
|
82
93
|
program,
|
|
83
94
|
tsDataForgeImport,
|
|
84
|
-
|
|
95
|
+
importsToAdd
|
|
85
96
|
) : [];
|
|
86
97
|
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
87
98
|
}
|
|
@@ -92,6 +103,9 @@ const preferIsRecordAndHasKey = {
|
|
|
92
103
|
},
|
|
93
104
|
defaultOptions: []
|
|
94
105
|
};
|
|
106
|
+
const isAlreadyRecordType = (checker, type) => type.isUnion() ? type.types.every((member) => isAlreadyRecordType(checker, member)) : checker.getIndexInfoOfType(type, ts.IndexKind.String) !== void 0 && Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) && Arr.isEmpty(
|
|
107
|
+
checker.getSignaturesOfType(type, ts.SignatureKind.Construct)
|
|
108
|
+
) && !checker.isArrayType(type) && !checker.isTupleType(type);
|
|
95
109
|
|
|
96
110
|
export { preferIsRecordAndHasKey };
|
|
97
111
|
//# sourceMappingURL=prefer-is-record-and-has-key.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-record-and-has-key.mjs","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useIsRecordAndHasKey';\n\nexport const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useIsRecordAndHasKey:\n 'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression | TSESTree.BinaryExpression;\n objExpression: TSESTree.Expression;\n keyExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n // Handle Object.hasOwn(obj, key)\n CallExpression: (node) => {\n if (\n node.callee.type !== AST_NODE_TYPES.MemberExpression ||\n node.callee.object.type !== AST_NODE_TYPES.Identifier ||\n node.callee.object.name !== 'Object' ||\n node.callee.property.type !== AST_NODE_TYPES.Identifier ||\n node.callee.property.name !== 'hasOwn'\n ) {\n return;\n }\n\n // Check arguments: Object.hasOwn(obj, key)\n if (node.arguments.length !== 2) return;\n\n const objArg = node.arguments[0];\n\n const keyArg = node.arguments[1];\n\n if (objArg === undefined || keyArg === undefined) return;\n\n // Type guard: ensure arguments are Expressions (not SpreadElement)\n if (\n objArg.type === AST_NODE_TYPES.SpreadElement ||\n keyArg.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n mut_nodesToFix.push({\n node,\n objExpression: objArg,\n keyExpression: keyArg,\n });\n },\n\n // Handle key in obj\n BinaryExpression: (node) => {\n if (node.operator !== 'in') return;\n\n const keyExpression = node.left;\n\n const objExpression = node.right;\n\n // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)\n if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;\n\n mut_nodesToFix.push({\n node,\n objExpression,\n keyExpression,\n });\n },\n\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const hasIsRecordImport = namedImports.includes('isRecord');\n\n const hasHasKeyImport = namedImports.includes('hasKey');\n\n for (const [\n index,\n { node, objExpression, keyExpression },\n ] of mut_nodesToFix.entries()) {\n const objText = sourceCode.getText(objExpression);\n\n const keyText = sourceCode.getText(keyExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: 'useIsRecordAndHasKey',\n data: {\n original: originalText,\n objName: objText,\n keyName: keyText,\n },\n fix: (fixer) => {\n const replacement = `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`;\n\n const mut_importsToAdd: string[] = [];\n\n if (!hasIsRecordImport) {\n mut_importsToAdd.push('isRecord');\n }\n\n if (!hasHasKeyImport) {\n mut_importsToAdd.push('hasKey');\n }\n\n const importFixes =\n index === 0 && mut_importsToAdd.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n mut_importsToAdd,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n } as const;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prefer-is-record-and-has-key.mjs","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport * as ts from 'typescript';\nimport {\n buildImportFixes,\n getNamedImports,\n getTsDataForgeImport,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useIsRecordAndHasKey' | 'useHasKey';\n\nexport const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =\n {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useIsRecordAndHasKey:\n 'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n useHasKey:\n 'Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsDataForgeImport = getTsDataForgeImport(program);\n\n const services = sourceCode.parserServices;\n\n const checker = services?.program?.getTypeChecker();\n\n /**\n * Whether the `isRecord(...)` half of the rewrite would be dead code\n * because the object already satisfies `hasKey`'s\n * `R extends UnknownRecord` constraint. Without type information nothing\n * is known, so the guard is kept.\n */\n const isKnownRecord = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n expression: TSESTree.Expression,\n ): boolean => {\n if (checker === undefined) return false;\n\n const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);\n\n return (\n tsNode !== undefined &&\n isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))\n );\n };\n\n const mut_nodesToFix: {\n node: TSESTree.CallExpression | TSESTree.BinaryExpression;\n objExpression: TSESTree.Expression;\n keyExpression: TSESTree.Expression;\n }[] = [];\n\n return {\n // Handle Object.hasOwn(obj, key)\n CallExpression: (node) => {\n if (\n node.callee.type !== AST_NODE_TYPES.MemberExpression ||\n node.callee.object.type !== AST_NODE_TYPES.Identifier ||\n node.callee.object.name !== 'Object' ||\n node.callee.property.type !== AST_NODE_TYPES.Identifier ||\n node.callee.property.name !== 'hasOwn'\n ) {\n return;\n }\n\n // Check arguments: Object.hasOwn(obj, key)\n if (node.arguments.length !== 2) return;\n\n const objArg = node.arguments[0];\n\n const keyArg = node.arguments[1];\n\n if (objArg === undefined || keyArg === undefined) return;\n\n // Type guard: ensure arguments are Expressions (not SpreadElement)\n if (\n objArg.type === AST_NODE_TYPES.SpreadElement ||\n keyArg.type === AST_NODE_TYPES.SpreadElement\n ) {\n return;\n }\n\n mut_nodesToFix.push({\n node,\n objExpression: objArg,\n keyExpression: keyArg,\n });\n },\n\n // Handle key in obj\n BinaryExpression: (node) => {\n if (node.operator !== 'in') return;\n\n const keyExpression = node.left;\n\n const objExpression = node.right;\n\n // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)\n if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;\n\n mut_nodesToFix.push({\n node,\n objExpression,\n keyExpression,\n });\n },\n\n 'Program:exit': () => {\n const namedImports = getNamedImports(tsDataForgeImport);\n\n const rewrites = mut_nodesToFix.map((entry) => ({\n ...entry,\n // A record-typed object makes the `isRecord(...)` conjunct always\n // true, so it is left out of the replacement entirely.\n needsIsRecord: !isKnownRecord(entry.objExpression),\n }));\n\n const importsToAdd: readonly string[] = [\n ...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&\n !namedImports.includes('isRecord')\n ? ['isRecord']\n : []),\n ...(namedImports.includes('hasKey') ? [] : ['hasKey']),\n ];\n\n for (const [\n index,\n { node, objExpression, keyExpression, needsIsRecord },\n ] of rewrites.entries()) {\n const objText = sourceCode.getText(objExpression);\n\n const keyText = sourceCode.getText(keyExpression);\n\n const originalText = sourceCode.getText(node);\n\n context.report({\n node,\n messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',\n data: {\n original: originalText,\n objName: objText,\n keyName: keyText,\n },\n fix: (fixer) => {\n const replacement = needsIsRecord\n ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`\n : `hasKey(${objText}, ${keyText})`;\n\n // All import fixes have to live on the first report:\n // `insertTextBefore(program, …)` produces overlapping ranges\n // otherwise and only one of them would survive a pass.\n const importFixes =\n index === 0 && importsToAdd.length > 0\n ? buildImportFixes(\n fixer,\n program,\n tsDataForgeImport,\n importsToAdd,\n )\n : [];\n\n return [...importFixes, fixer.replaceText(node, replacement)];\n },\n });\n }\n },\n };\n },\n defaultOptions: [],\n } as const;\n\n/**\n * Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`\n * constraint, so that guarding the call with `isRecord` adds nothing.\n *\n * `UnknownRecord` is `ReadonlyRecord<string, unknown>`, so a string index\n * signature is exactly what it asks for and its value type is `unknown`, which\n * anything satisfies. Callables and arrays are excluded because `isRecord`\n * rejects them at runtime (`typeof fn === 'object'` is false, `Array.isArray`\n * is true), so dropping the guard there would change behavior.\n *\n * Deliberately conservative: a type TypeScript would accept through an\n * *implicit* index signature (an object type alias without one of its own)\n * keeps the guard, because the checker does not report such a signature here.\n */\nconst isAlreadyRecordType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n checker: ts.TypeChecker,\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: ts.Type,\n): boolean =>\n type.isUnion()\n ? type.types.every((member) => isAlreadyRecordType(checker, member))\n : checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&\n Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) &&\n Arr.isEmpty(\n checker.getSignaturesOfType(type, ts.SignatureKind.Construct),\n ) &&\n !checker.isArrayType(type) &&\n !checker.isTupleType(type);\n"],"names":[],"mappings":"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"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const tsDataForgeRules: {
|
|
|
7
7
|
readonly 'prefer-as-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useBrandedNumberCastFunction", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
8
8
|
readonly 'prefer-is-non-null-object': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNonNullObject", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
9
9
|
readonly 'prefer-range-for-loop': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useRangeForLoop", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
10
|
-
readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
10
|
+
readonly 'prefer-is-record-and-has-key': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useHasKey" | "useIsRecordAndHasKey", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
11
11
|
readonly 'prefer-num-safe-parse-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseInt", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
12
12
|
readonly 'prefer-num-safe-parse-float': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useSafeParseFloat", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
13
13
|
readonly 'no-unnecessary-type-guard': import("@typescript-eslint/utils/ts-eslint").RuleModule<"alwaysFalse" | "alwaysTrue" | "replaceTypeGuard", readonly [(Readonly<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ts-data-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer code toward ts-data-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"LICENSE"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@typescript-eslint/utils": "8.
|
|
38
|
-
"ts-type-forge": "
|
|
39
|
-
"ts-data-forge": "14.0.
|
|
37
|
+
"@typescript-eslint/utils": "^8.65.0",
|
|
38
|
+
"ts-type-forge": "~9.1.1",
|
|
39
|
+
"ts-data-forge": "14.0.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@typescript-eslint/parser": "8.
|
|
43
|
-
"@typescript-eslint/rule-tester": "8.
|
|
42
|
+
"@typescript-eslint/parser": "8.65.0",
|
|
43
|
+
"@typescript-eslint/rule-tester": "8.65.0",
|
|
44
44
|
"dedent": "1.7.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -44,6 +44,7 @@ type GuardSpec = DeepReadonly<
|
|
|
44
44
|
replacements?: Record<string, string>;
|
|
45
45
|
}
|
|
46
46
|
| { kind: 'nonEmptyString' }
|
|
47
|
+
| { kind: 'record' }
|
|
47
48
|
>;
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -98,6 +99,7 @@ const GUARD_SPECS: DeepReadonly<Record<string, GuardSpec>> = {
|
|
|
98
99
|
replacements: NULLISH_REPLACEMENTS_EXCLUDE,
|
|
99
100
|
},
|
|
100
101
|
isNonEmptyString: { kind: 'nonEmptyString' },
|
|
102
|
+
isRecord: { kind: 'record' },
|
|
101
103
|
} as const;
|
|
102
104
|
|
|
103
105
|
const DEFERRED_OR_OPAQUE_FLAGS =
|
|
@@ -341,6 +343,20 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
341
343
|
break;
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
case 'record': {
|
|
347
|
+
if (parts.every((p) => isGuaranteedRecord(p, checker))) {
|
|
348
|
+
// Every member already satisfies `UnknownRecord`.
|
|
349
|
+
reportConstant(node, canonicalName, true, argument);
|
|
350
|
+
} else if (
|
|
351
|
+
parts.every((p) => isGuaranteedNotRecord(p, checker))
|
|
352
|
+
) {
|
|
353
|
+
// No member can ever be a non-null, non-array object.
|
|
354
|
+
reportConstant(node, canonicalName, false, argument);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
344
360
|
case 'nonEmptyString': {
|
|
345
361
|
const hasNullish =
|
|
346
362
|
inputAtoms.has('undefined') || inputAtoms.has('null');
|
|
@@ -403,6 +419,55 @@ const collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {
|
|
|
403
419
|
return parts;
|
|
404
420
|
};
|
|
405
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Whether `isRecord` is guaranteed to accept this (non-union) type.
|
|
424
|
+
*
|
|
425
|
+
* `isRecord` narrows to `UnknownRecord` (`ReadonlyRecord<string, unknown>`), so
|
|
426
|
+
* a string index signature is exactly what it asks for and its value type is
|
|
427
|
+
* `unknown`, which anything satisfies. Arrays and callables are excluded even
|
|
428
|
+
* when they carry one, because `isRecord` rejects them at runtime
|
|
429
|
+
* (`Array.isArray` is true; `typeof fn === 'object'` is false).
|
|
430
|
+
*
|
|
431
|
+
* Deliberately conservative: an object type without an index signature of its
|
|
432
|
+
* own is reported as neither guaranteed-accepted nor guaranteed-rejected, even
|
|
433
|
+
* where TypeScript would accept it through an implicit index signature.
|
|
434
|
+
*/
|
|
435
|
+
const isGuaranteedRecord = (
|
|
436
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
437
|
+
type: ts.Type,
|
|
438
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
439
|
+
checker: ts.TypeChecker,
|
|
440
|
+
): boolean =>
|
|
441
|
+
checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
|
|
442
|
+
!isCallableType(type, checker) &&
|
|
443
|
+
!checker.isArrayType(type) &&
|
|
444
|
+
!checker.isTupleType(type);
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether `isRecord` is guaranteed to reject this (non-union) type: every
|
|
448
|
+
* primitive (`null` included), every array or tuple, and every callable fails
|
|
449
|
+
* its `typeof x === 'object' && x !== null && !Array.isArray(x)` check.
|
|
450
|
+
*/
|
|
451
|
+
const isGuaranteedNotRecord = (
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
453
|
+
type: ts.Type,
|
|
454
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
455
|
+
checker: ts.TypeChecker,
|
|
456
|
+
): boolean =>
|
|
457
|
+
classifyAtom(type) !== 'other' ||
|
|
458
|
+
checker.isArrayType(type) ||
|
|
459
|
+
checker.isTupleType(type) ||
|
|
460
|
+
isCallableType(type, checker);
|
|
461
|
+
|
|
462
|
+
const isCallableType = (
|
|
463
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
464
|
+
type: ts.Type,
|
|
465
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
466
|
+
checker: ts.TypeChecker,
|
|
467
|
+
): boolean =>
|
|
468
|
+
checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||
|
|
469
|
+
checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;
|
|
470
|
+
|
|
406
471
|
/** Classifies a single (non-union) type into a primitive {@link Atom}. */
|
|
407
472
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
408
473
|
const classifyAtom = (type: ts.Type): Atom => {
|
|
@@ -20,6 +20,30 @@ const tester = new RuleTester({
|
|
|
20
20
|
describe('no-unnecessary-type-guard', () => {
|
|
21
21
|
tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
|
|
22
22
|
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'isRecord that actually narrows a union',
|
|
25
|
+
code: dedent`
|
|
26
|
+
import { isRecord } from 'ts-data-forge';
|
|
27
|
+
declare const x: Record<string, unknown> | string;
|
|
28
|
+
const y = isRecord(x);
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'isRecord on an object type without an index signature of its own',
|
|
33
|
+
code: dedent`
|
|
34
|
+
import { isRecord } from 'ts-data-forge';
|
|
35
|
+
declare const x: Readonly<{ a: number }>;
|
|
36
|
+
const y = isRecord(x);
|
|
37
|
+
`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'isRecord on unknown',
|
|
41
|
+
code: dedent`
|
|
42
|
+
import { isRecord } from 'ts-data-forge';
|
|
43
|
+
declare const x: unknown;
|
|
44
|
+
const y = isRecord(x);
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
23
47
|
{
|
|
24
48
|
name: 'isNotUndefined on a type that includes undefined',
|
|
25
49
|
code: dedent`
|
|
@@ -110,6 +134,76 @@ describe('no-unnecessary-type-guard', () => {
|
|
|
110
134
|
},
|
|
111
135
|
],
|
|
112
136
|
invalid: [
|
|
137
|
+
{
|
|
138
|
+
name: 'isRecord on a record type (always true)',
|
|
139
|
+
code: dedent`
|
|
140
|
+
import { isRecord } from 'ts-data-forge';
|
|
141
|
+
declare const x: Readonly<Record<string, unknown>>;
|
|
142
|
+
const y = isRecord(x);
|
|
143
|
+
`,
|
|
144
|
+
output: dedent`
|
|
145
|
+
import { isRecord } from 'ts-data-forge';
|
|
146
|
+
declare const x: Readonly<Record<string, unknown>>;
|
|
147
|
+
const y = true;
|
|
148
|
+
`,
|
|
149
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'isRecord on a union of record types (always true)',
|
|
153
|
+
code: dedent`
|
|
154
|
+
import { isRecord } from 'ts-data-forge';
|
|
155
|
+
declare const x: Record<string, number> | Record<string, string>;
|
|
156
|
+
const y = isRecord(x);
|
|
157
|
+
`,
|
|
158
|
+
output: dedent`
|
|
159
|
+
import { isRecord } from 'ts-data-forge';
|
|
160
|
+
declare const x: Record<string, number> | Record<string, string>;
|
|
161
|
+
const y = true;
|
|
162
|
+
`,
|
|
163
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'isRecord on a primitive (always false)',
|
|
167
|
+
code: dedent`
|
|
168
|
+
import { isRecord } from 'ts-data-forge';
|
|
169
|
+
declare const x: string | null;
|
|
170
|
+
const y = isRecord(x);
|
|
171
|
+
`,
|
|
172
|
+
output: dedent`
|
|
173
|
+
import { isRecord } from 'ts-data-forge';
|
|
174
|
+
declare const x: string | null;
|
|
175
|
+
const y = false;
|
|
176
|
+
`,
|
|
177
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'isRecord on an array (always false)',
|
|
181
|
+
code: dedent`
|
|
182
|
+
import { isRecord } from 'ts-data-forge';
|
|
183
|
+
declare const x: readonly number[];
|
|
184
|
+
const y = isRecord(x);
|
|
185
|
+
`,
|
|
186
|
+
output: dedent`
|
|
187
|
+
import { isRecord } from 'ts-data-forge';
|
|
188
|
+
declare const x: readonly number[];
|
|
189
|
+
const y = false;
|
|
190
|
+
`,
|
|
191
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'isRecord on a function (always false)',
|
|
195
|
+
code: dedent`
|
|
196
|
+
import { isRecord } from 'ts-data-forge';
|
|
197
|
+
declare const x: () => void;
|
|
198
|
+
const y = isRecord(x);
|
|
199
|
+
`,
|
|
200
|
+
output: dedent`
|
|
201
|
+
import { isRecord } from 'ts-data-forge';
|
|
202
|
+
declare const x: () => void;
|
|
203
|
+
const y = false;
|
|
204
|
+
`,
|
|
205
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
206
|
+
},
|
|
113
207
|
{
|
|
114
208
|
name: 'isNotUndefined on a type that cannot be undefined (always true)',
|
|
115
209
|
code: dedent`
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import { Arr } from 'ts-data-forge';
|
|
7
|
+
import * as ts from 'typescript';
|
|
6
8
|
import {
|
|
7
9
|
buildImportFixes,
|
|
8
10
|
getNamedImports,
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
|
|
12
14
|
type Options = readonly [];
|
|
13
15
|
|
|
14
|
-
type MessageIds = 'useIsRecordAndHasKey';
|
|
16
|
+
type MessageIds = 'useIsRecordAndHasKey' | 'useHasKey';
|
|
15
17
|
|
|
16
18
|
export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
17
19
|
{
|
|
@@ -26,6 +28,8 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
26
28
|
messages: {
|
|
27
29
|
useIsRecordAndHasKey:
|
|
28
30
|
'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',
|
|
31
|
+
useHasKey:
|
|
32
|
+
'Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge.',
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
|
|
@@ -36,6 +40,30 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
36
40
|
|
|
37
41
|
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
38
42
|
|
|
43
|
+
const services = sourceCode.parserServices;
|
|
44
|
+
|
|
45
|
+
const checker = services?.program?.getTypeChecker();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
49
|
+
* because the object already satisfies `hasKey`'s
|
|
50
|
+
* `R extends UnknownRecord` constraint. Without type information nothing
|
|
51
|
+
* is known, so the guard is kept.
|
|
52
|
+
*/
|
|
53
|
+
const isKnownRecord = (
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
55
|
+
expression: TSESTree.Expression,
|
|
56
|
+
): boolean => {
|
|
57
|
+
if (checker === undefined) return false;
|
|
58
|
+
|
|
59
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
tsNode !== undefined &&
|
|
63
|
+
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
39
67
|
const mut_nodesToFix: {
|
|
40
68
|
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
41
69
|
objExpression: TSESTree.Expression;
|
|
@@ -100,14 +128,25 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
100
128
|
'Program:exit': () => {
|
|
101
129
|
const namedImports = getNamedImports(tsDataForgeImport);
|
|
102
130
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
const rewrites = mut_nodesToFix.map((entry) => ({
|
|
132
|
+
...entry,
|
|
133
|
+
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
134
|
+
// true, so it is left out of the replacement entirely.
|
|
135
|
+
needsIsRecord: !isKnownRecord(entry.objExpression),
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
const importsToAdd: readonly string[] = [
|
|
139
|
+
...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&
|
|
140
|
+
!namedImports.includes('isRecord')
|
|
141
|
+
? ['isRecord']
|
|
142
|
+
: []),
|
|
143
|
+
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
144
|
+
];
|
|
106
145
|
|
|
107
146
|
for (const [
|
|
108
147
|
index,
|
|
109
|
-
{ node, objExpression, keyExpression },
|
|
110
|
-
] of
|
|
148
|
+
{ node, objExpression, keyExpression, needsIsRecord },
|
|
149
|
+
] of rewrites.entries()) {
|
|
111
150
|
const objText = sourceCode.getText(objExpression);
|
|
112
151
|
|
|
113
152
|
const keyText = sourceCode.getText(keyExpression);
|
|
@@ -116,32 +155,27 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
116
155
|
|
|
117
156
|
context.report({
|
|
118
157
|
node,
|
|
119
|
-
messageId: 'useIsRecordAndHasKey',
|
|
158
|
+
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
120
159
|
data: {
|
|
121
160
|
original: originalText,
|
|
122
161
|
objName: objText,
|
|
123
162
|
keyName: keyText,
|
|
124
163
|
},
|
|
125
164
|
fix: (fixer) => {
|
|
126
|
-
const replacement =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (!hasIsRecordImport) {
|
|
131
|
-
mut_importsToAdd.push('isRecord');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!hasHasKeyImport) {
|
|
135
|
-
mut_importsToAdd.push('hasKey');
|
|
136
|
-
}
|
|
165
|
+
const replacement = needsIsRecord
|
|
166
|
+
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
167
|
+
: `hasKey(${objText}, ${keyText})`;
|
|
137
168
|
|
|
169
|
+
// All import fixes have to live on the first report:
|
|
170
|
+
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
171
|
+
// otherwise and only one of them would survive a pass.
|
|
138
172
|
const importFixes =
|
|
139
|
-
index === 0 &&
|
|
173
|
+
index === 0 && importsToAdd.length > 0
|
|
140
174
|
? buildImportFixes(
|
|
141
175
|
fixer,
|
|
142
176
|
program,
|
|
143
177
|
tsDataForgeImport,
|
|
144
|
-
|
|
178
|
+
importsToAdd,
|
|
145
179
|
)
|
|
146
180
|
: [];
|
|
147
181
|
|
|
@@ -154,3 +188,33 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
154
188
|
},
|
|
155
189
|
defaultOptions: [],
|
|
156
190
|
} as const;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
194
|
+
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
195
|
+
*
|
|
196
|
+
* `UnknownRecord` is `ReadonlyRecord<string, unknown>`, so a string index
|
|
197
|
+
* signature is exactly what it asks for and its value type is `unknown`, which
|
|
198
|
+
* anything satisfies. Callables and arrays are excluded because `isRecord`
|
|
199
|
+
* rejects them at runtime (`typeof fn === 'object'` is false, `Array.isArray`
|
|
200
|
+
* is true), so dropping the guard there would change behavior.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately conservative: a type TypeScript would accept through an
|
|
203
|
+
* *implicit* index signature (an object type alias without one of its own)
|
|
204
|
+
* keeps the guard, because the checker does not report such a signature here.
|
|
205
|
+
*/
|
|
206
|
+
const isAlreadyRecordType = (
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
208
|
+
checker: ts.TypeChecker,
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
210
|
+
type: ts.Type,
|
|
211
|
+
): boolean =>
|
|
212
|
+
type.isUnion()
|
|
213
|
+
? type.types.every((member) => isAlreadyRecordType(checker, member))
|
|
214
|
+
: checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
|
|
215
|
+
Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) &&
|
|
216
|
+
Arr.isEmpty(
|
|
217
|
+
checker.getSignaturesOfType(type, ts.SignatureKind.Construct),
|
|
218
|
+
) &&
|
|
219
|
+
!checker.isArrayType(type) &&
|
|
220
|
+
!checker.isTupleType(type);
|
|
@@ -36,6 +36,80 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
36
36
|
},
|
|
37
37
|
],
|
|
38
38
|
invalid: [
|
|
39
|
+
{
|
|
40
|
+
name: 'drops isRecord when the object already has a string index signature',
|
|
41
|
+
code: dedent`
|
|
42
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
43
|
+
const ok = Object.hasOwn(obj, 'a');
|
|
44
|
+
`,
|
|
45
|
+
output: dedent`
|
|
46
|
+
import { hasKey } from 'ts-data-forge';
|
|
47
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
48
|
+
const ok = hasKey(obj, 'a');
|
|
49
|
+
`,
|
|
50
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'drops isRecord for `key in obj` on a record-typed object',
|
|
54
|
+
code: dedent`
|
|
55
|
+
declare const obj: Record<string, number>;
|
|
56
|
+
const ok = 'a' in obj;
|
|
57
|
+
`,
|
|
58
|
+
output: dedent`
|
|
59
|
+
import { hasKey } from 'ts-data-forge';
|
|
60
|
+
declare const obj: Record<string, number>;
|
|
61
|
+
const ok = hasKey(obj, 'a');
|
|
62
|
+
`,
|
|
63
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'keeps isRecord when only part of a union is a record',
|
|
67
|
+
code: dedent`
|
|
68
|
+
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
69
|
+
const ok = Object.hasOwn(obj, 'a');
|
|
70
|
+
`,
|
|
71
|
+
output: dedent`
|
|
72
|
+
import { isRecord, hasKey } from 'ts-data-forge';
|
|
73
|
+
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
74
|
+
const ok = (isRecord(obj) && hasKey(obj, 'a'));
|
|
75
|
+
`,
|
|
76
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'keeps isRecord for an index-signature-carrying callable',
|
|
80
|
+
code: dedent`
|
|
81
|
+
type Callable = { (): void; [key: string]: unknown };
|
|
82
|
+
declare const obj: Callable;
|
|
83
|
+
const ok = Object.hasOwn(obj, 'a');
|
|
84
|
+
`,
|
|
85
|
+
output: dedent`
|
|
86
|
+
import { isRecord, hasKey } from 'ts-data-forge';
|
|
87
|
+
type Callable = { (): void; [key: string]: unknown };
|
|
88
|
+
declare const obj: Callable;
|
|
89
|
+
const ok = (isRecord(obj) && hasKey(obj, 'a'));
|
|
90
|
+
`,
|
|
91
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'imports isRecord once when both forms appear in a file',
|
|
95
|
+
code: dedent`
|
|
96
|
+
declare const rec: Record<string, unknown>;
|
|
97
|
+
declare const unk: unknown;
|
|
98
|
+
const a = Object.hasOwn(rec, 'a');
|
|
99
|
+
const b = Object.hasOwn(unk, 'b');
|
|
100
|
+
`,
|
|
101
|
+
output: dedent`
|
|
102
|
+
import { isRecord, hasKey } from 'ts-data-forge';
|
|
103
|
+
declare const rec: Record<string, unknown>;
|
|
104
|
+
declare const unk: unknown;
|
|
105
|
+
const a = hasKey(rec, 'a');
|
|
106
|
+
const b = (isRecord(unk) && hasKey(unk, 'b'));
|
|
107
|
+
`,
|
|
108
|
+
errors: [
|
|
109
|
+
{ messageId: 'useHasKey' },
|
|
110
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
39
113
|
{
|
|
40
114
|
name: 'replaces Object.hasOwn(obj, key) with isRecord && hasKey',
|
|
41
115
|
code: dedent`
|