eslint-plugin-ts-fortress 0.3.0 → 1.0.0

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/README.md CHANGED
@@ -15,6 +15,39 @@ configured TypeScript project is not required.
15
15
 
16
16
  ## Usage (flat config)
17
17
 
18
+ The plugin ships a `recommended` config preset that registers the plugin and
19
+ turns on **every** rule at `error`:
20
+
21
+ ```ts
22
+ // eslint.config.mts
23
+ import { eslintPluginTsFortress } from 'eslint-plugin-ts-fortress';
24
+
25
+ export default [eslintPluginTsFortress.configs.recommended];
26
+ ```
27
+
28
+ Since the preset is a plain flat-config object, individual rules can be
29
+ adjusted by a later config entry:
30
+
31
+ ```ts
32
+ // eslint.config.mts
33
+ import {
34
+ eslintPluginTsFortress,
35
+ type EslintTsFortressRules,
36
+ } from 'eslint-plugin-ts-fortress';
37
+
38
+ export default [
39
+ eslintPluginTsFortress.configs.recommended,
40
+ {
41
+ files: ['src/legacy/**'],
42
+ rules: {
43
+ 'ts-fortress/prefer-canonical-length-constrained-type': 'off',
44
+ } satisfies Partial<EslintTsFortressRules>,
45
+ },
46
+ ];
47
+ ```
48
+
49
+ Or register the plugin yourself and pick the rules one by one:
50
+
18
51
  ```ts
19
52
  // eslint.config.mts
20
53
  import {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { eslintPluginTsFortress } from './plugin.mjs';
2
2
  export type { EslintTsFortressRules, EslintTsFortressRulesOption, } from './rule-types.mjs';
3
3
  export { tsFortressRules } from './rules/index.mjs';
4
- export { type ESLintPlugin } from './types.mjs';
4
+ export { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
5
5
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/dist/plugin.d.mts CHANGED
@@ -1,3 +1,22 @@
1
1
  import { type ESLintPlugin } from './types.mjs';
2
- export declare const eslintPluginTsFortress: ESLintPlugin;
2
+ export declare const eslintPluginTsFortress: {
3
+ readonly meta: {
4
+ readonly name: 'eslint-plugin-ts-fortress';
5
+ };
6
+ readonly rules: {
7
+ readonly 'prefer-canonical-length-constrained-type': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalType", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
8
+ };
9
+ readonly configs: {
10
+ /** Enables every rule of this plugin at `error`. */
11
+ readonly recommended: {
12
+ readonly name: 'ts-fortress/recommended';
13
+ readonly plugins: {
14
+ readonly 'ts-fortress': ESLintPlugin;
15
+ };
16
+ readonly rules: {
17
+ readonly 'ts-fortress/prefer-canonical-length-constrained-type': 'error';
18
+ };
19
+ };
20
+ };
21
+ };
3
22
  //# sourceMappingURL=plugin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,sBAAsB,EAAE,YAK3B,CAAC"}
1
+ {"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AAEA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AA+BvE,eAAO,MAAM,sBAAsB;;iBAE/B,IAAI,EAAE,2BAA2B;;;;;;QAIjC,oDAAoD;iBACpD,WAAW;2BAtBP,yBAAyB;;yBAQzB,aAAa,EAAI,YAAY;;;iFAfqB,OAAO;;;;CA+BhC,CAAC"}
package/dist/plugin.mjs CHANGED
@@ -1,10 +1,32 @@
1
1
  import { tsFortressRules } from './rules/rules.mjs';
2
2
 
3
+ const recommendedRules = {
4
+ "ts-fortress/prefer-canonical-length-constrained-type": "error"
5
+ };
6
+ const recommendedConfig = {
7
+ name: "ts-fortress/recommended",
8
+ plugins: {
9
+ // Resolved lazily so that this config registers the *same* object that
10
+ // `eslintPluginTsFortress` refers to. ESLint rejects a plugin name that
11
+ // maps to two different objects with `Cannot redefine plugin`, which is
12
+ // what a user combining this preset with their own
13
+ // `plugins: { 'ts-fortress': eslintPluginTsFortress }` entry would hit if
14
+ // the preset embedded a separate copy of the plugin.
15
+ get "ts-fortress"() {
16
+ return eslintPluginTsFortress;
17
+ }
18
+ },
19
+ rules: recommendedRules
20
+ };
3
21
  const eslintPluginTsFortress = {
4
22
  meta: {
5
23
  name: "eslint-plugin-ts-fortress"
6
24
  },
7
- rules: tsFortressRules
25
+ rules: tsFortressRules,
26
+ configs: {
27
+ /** Enables every rule of this plugin at `error`. */
28
+ recommended: recommendedConfig
29
+ }
8
30
  };
9
31
 
10
32
  export { eslintPluginTsFortress };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { tsFortressRules } from './rules/index.mjs';\nimport { type ESLintPlugin } from './types.mjs';\n\nexport const eslintPluginTsFortress: ESLintPlugin = {\n meta: {\n name: 'eslint-plugin-ts-fortress',\n },\n rules: tsFortressRules,\n} as const;\n"],"names":[],"mappings":";;AAGO,MAAM,sBAAA,GAAuC;AAAA,EAClD,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO;AACT;;;;"}
1
+ {"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { type ReadonlyRecord } from 'ts-type-forge';\nimport { tsFortressRules } from './rules/index.mjs';\nimport { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';\n\n/**\n * Every rule this plugin ships, at `error`.\n *\n * The `satisfies` clause below is keyed off {@link tsFortressRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-fortress/prefer-canonical-length-constrained-type': 'error',\n} as const satisfies ReadonlyRecord<\n `ts-fortress/${keyof typeof tsFortressRules}`,\n 'error'\n>;\n\nconst recommendedConfig = {\n name: 'ts-fortress/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsFortress` refers to. ESLint rejects a plugin name that\n // maps to two different objects with `Cannot redefine plugin`, which is\n // what a user combining this preset with their own\n // `plugins: { 'ts-fortress': eslintPluginTsFortress }` entry would hit if\n // the preset embedded a separate copy of the plugin.\n get 'ts-fortress'(): ESLintPlugin {\n return eslintPluginTsFortress;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsFortress = {\n meta: {\n name: 'eslint-plugin-ts-fortress',\n },\n rules: tsFortressRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AAUA,MAAM,gBAAA,GAAmB;AAAA,EACvB,sDAAA,EAAwD;AAC1D,CAAA;AAKA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,yBAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,aAAA,GAA8B;AAChC,MAAA,OAAO,sBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,sBAAA,GAAyB;AAAA,EACpC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,eAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB;;;;"}
@@ -1,5 +1,5 @@
1
1
  import { type Linter } from 'eslint';
2
- import { type TypeEq } from 'ts-type-forge';
2
+ import { type ReadonlyRecord, type TypeEq } from 'ts-type-forge';
3
3
  import { type tsFortressRules } from './rules/index.mjs';
4
4
  /**
5
5
  * @description Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.
@@ -18,7 +18,7 @@ declare namespace PreferCanonicalLengthConstrainedType {
18
18
  export type EslintTsFortressRules = Readonly<{
19
19
  'ts-fortress/prefer-canonical-length-constrained-type': PreferCanonicalLengthConstrainedType.RuleEntry;
20
20
  }>;
21
- export type EslintTsFortressRulesOption = Readonly<Record<never, never>>;
21
+ export type EslintTsFortressRulesOption = ReadonlyRecord<never, never>;
22
22
  type AssertTrue<B extends true> = B;
23
23
  export type RuleNamesAreInSync = AssertTrue<TypeEq<keyof EslintTsFortressRules, `ts-fortress/${keyof typeof tsFortressRules}`>>;
24
24
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,kBAAU,oCAAoC,CAAC;IAC7C,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,sDAAsD,EAAE,oCAAoC,CAAC,SAAS,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAIzE,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,qBAAqB,EAC3B,eAAe,MAAM,OAAO,eAAe,EAAE,CAC9C,CACF,CAAC"}
1
+ {"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,kBAAU,oCAAoC,CAAC;IAC7C,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,sDAAsD,EAAE,oCAAoC,CAAC,SAAS,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAIvE,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,qBAAqB,EAC3B,eAAe,MAAM,OAAO,eAAe,EAAE,CAC9C,CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,YACtB,QAAQ,CAAC,OAAO,KACxB,SAAS,QAAQ,CAAC,iBAAiB,EAKnC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uBACX,SAAS,QAAQ,CAAC,iBAAiB,EAAE,gBAC3C,MAAM,KACnB,MAAM,GAAG,SASK,CAAC;AAElB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,uBACV,SAAS,QAAQ,CAAC,iBAAiB,EAAE,KACxD,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,KAAK,cAAc,GAAG,SAAS,CAkD9D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAChB,QAAQ,CAAC,UAAU,QACzB,QAAQ,CAAC,IAAI,QACb,MAAM,KACX,OAgBF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,UACnB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,SAClB,SAAS,MAAM,EAAE,KACvB,SAAS,QAAQ,CAAC,OAAO,EAUX,CAAC"}
1
+ {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAMlC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,YACtB,QAAQ,CAAC,OAAO,KACxB,SAAS,QAAQ,CAAC,iBAAiB,EAKnC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uBACX,SAAS,QAAQ,CAAC,iBAAiB,EAAE,gBAC3C,MAAM,KACnB,MAAM,GAAG,SASK,CAAC;AAElB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,uBACV,SAAS,QAAQ,CAAC,iBAAiB,EAAE,KACxD,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,KAAK,cAAc,GAAG,SAAS,CAkD9D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAChB,QAAQ,CAAC,UAAU,QACzB,QAAQ,CAAC,IAAI,QACb,MAAM,KACX,OAgBF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,UACnB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,SAClB,SAAS,MAAM,EAAE,KACvB,SAAS,QAAQ,CAAC,OAAO,EAUX,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
2
3
  import { TS_FORTRESS_MODULE } from './constants.mjs';
3
4
 
4
5
  const getTsFortressImports = (program) => program.body.filter(
@@ -44,7 +45,7 @@ const hasForeignBinding = (sourceCode, node, name) => {
44
45
  }
45
46
  return false;
46
47
  };
47
- const buildImportsFix = (fixer, program, names) => names.length === 0 ? [] : [
48
+ const buildImportsFix = (fixer, program, names) => Arr.isEmpty(names) ? [] : [
48
49
  fixer.insertTextBefore(
49
50
  program,
50
51
  names.map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';
@@ -1 +1 @@
1
- {"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { TS_FORTRESS_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type ResolvedCallee = Readonly<{\n /** The canonical (imported) ts-fortress name, regardless of local alias. */\n canonicalName: string;\n /** The identifier node to rewrite when replacing the callee name. */\n propertyNode: TSESTree.Node;\n /** Whether the call was made through a namespace import (`t.foo(...)`). */\n isNamespace: boolean;\n}>;\n\n/** Every `import ... from 'ts-fortress'` declaration in the file. */\nexport const getTsFortressImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_FORTRESS_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-fortress export is imported as\n * (handling aliases such as `import { nonEmptyArray as nea }` → `'nea'`), or\n * `undefined` when it is not imported. Used so autofixes reference the binding\n * that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Builds a resolver that maps a call expression's callee to the canonical\n * ts-fortress function it refers to. Handles named imports (including aliases)\n * and namespace imports (`import * as t` → `t.foo(...)`) — the latter being the\n * idiomatic way to consume ts-fortress. Returns `undefined` for any callee that\n * does not reference the given ts-fortress imports.\n */\nexport const buildCalleeResolver = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n): ((callee: TSESTree.Expression) => ResolvedCallee | undefined) => {\n const specifiers = importDeclarations.flatMap(\n (declaration) => declaration.specifiers,\n );\n\n const namespaceNames = new Set(\n specifiers\n .filter(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,\n )\n .map((specifier) => specifier.local.name),\n );\n\n const localToCanonical = new Map<string, string>(\n specifiers.flatMap((specifier) => {\n if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) return [];\n\n const importedName =\n specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value;\n\n return typeof importedName === 'string'\n ? [[specifier.local.name, importedName] as const]\n : [];\n }),\n );\n\n return (callee) => {\n if (callee.type === AST_NODE_TYPES.Identifier) {\n const canonicalName = localToCanonical.get(callee.name);\n\n return canonicalName === undefined\n ? undefined\n : { canonicalName, propertyNode: callee, isNamespace: false };\n }\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n namespaceNames.has(callee.object.name) &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? {\n canonicalName: callee.property.name,\n propertyNode: callee.property,\n isNamespace: true,\n }\n : undefined;\n };\n};\n\n/**\n * Whether `name` resolves, at `node`, to a binding that is not a ts-fortress\n * import — a local declaration, a parameter, or an import from another module.\n * An autofix that makes a call reference `name` must bail out in that case,\n * because the rewritten call would resolve to the wrong binding.\n *\n * Scope-based rather than a scan of the top-level statements, so a binding that\n * merely shadows the ts-fortress one around the call site is caught too.\n */\nexport const hasForeignBinding = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n for (\n let mut_scope: TSESLint.Scope.Scope | null = sourceCode.getScope(node);\n mut_scope !== null;\n mut_scope = mut_scope.upper\n ) {\n const variable = mut_scope.set.get(name);\n\n // The innermost binding wins, so the first scope that declares `name`\n // decides — an outer ts-fortress import does not rescue a shadowed name.\n if (variable !== undefined) {\n return variable.defs.some((def) => !isTsFortressImportDef(def.node));\n }\n }\n\n return false;\n};\n\n/**\n * Inserts `import { <name> } from 'ts-fortress';` for each name at the top of\n * the file. Placement is deliberately naive: prettier-plugin-organize-imports\n * (or any equivalent) merges and sorts them afterwards.\n *\n * All names go into a single fix on purpose: ESLint merges the fixes of one\n * report into one edit span, so two reports each inserting at the start of the\n * file would conflict and one of them would be dropped.\n */\nexport const buildImportsFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n names: readonly string[],\n): readonly TSESLint.RuleFix[] =>\n names.length === 0\n ? ([] as const)\n : ([\n fixer.insertTextBefore(\n program,\n names\n .map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';\\n`)\n .join(''),\n ),\n ] as const);\n\nconst isTsFortressImportDef = (node: TSESTree.Node): boolean =>\n (node.type === AST_NODE_TYPES.ImportSpecifier ||\n node.type === AST_NODE_TYPES.ImportDefaultSpecifier ||\n node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) &&\n node.parent.type === AST_NODE_TYPES.ImportDeclaration &&\n node.parent.source.value === TS_FORTRESS_MODULE;\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":";;;AAmBO,MAAM,oBAAA,GAAuB,CAClC,OAAA,KAEA,OAAA,CAAQ,IAAA,CAAK,MAAA;AAAA,EACX,CAAC,SACC,IAAA,CAAK,IAAA,KAAS,eAAe,iBAAA,IAC7B,IAAA,CAAK,OAAO,KAAA,KAAU;AAC1B;AAQK,MAAM,oBAAA,GAAuB,CAClC,kBAAA,EACA,YAAA,KAEA,kBAAA,CACG,QAAQ,CAAC,WAAA,KAAgB,WAAA,CAAY,UAAU,CAAA,CAC/C,IAAA;AAAA,EACC,CAAC,SAAA,KACC,SAAA,CAAU,IAAA,KAAS,cAAA,CAAe,oBACjC,SAAA,CAAU,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,aACxC,SAAA,CAAU,QAAA,CAAS,IAAA,GACnB,SAAA,CAAU,SAAS,KAAA,MAAW;AACtC,CAAA,EAAG,KAAA,CAAM;AASN,MAAM,mBAAA,GAAsB,CACjC,kBAAA,KACkE;AAClE,EAAA,MAAM,aAAa,kBAAA,CAAmB,OAAA;AAAA,IACpC,CAAC,gBAAgB,WAAA,CAAY;AAAA,GAC/B;AAEA,EAAA,MAAM,iBAAiB,IAAI,GAAA;AAAA,IACzB,UAAA,CACG,MAAA;AAAA,MACC,CAAC,SAAA,KACC,SAAA,CAAU,IAAA,KAAS,cAAA,CAAe;AAAA,MAErC,GAAA,CAAI,CAAC,SAAA,KAAc,SAAA,CAAU,MAAM,IAAI;AAAA,GAC5C;AAEA,EAAA,MAAM,mBAAmB,IAAI,GAAA;AAAA,IAC3B,UAAA,CAAW,OAAA,CAAQ,CAAC,SAAA,KAAc;AAChC,MAAA,IAAI,SAAA,CAAU,IAAA,KAAS,cAAA,CAAe,eAAA,SAAwB,EAAC;AAE/D,MAAA,MAAM,YAAA,GACJ,SAAA,CAAU,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,aACvC,SAAA,CAAU,QAAA,CAAS,IAAA,GACnB,SAAA,CAAU,QAAA,CAAS,KAAA;AAEzB,MAAA,OAAO,OAAO,YAAA,KAAiB,QAAA,GAC3B,CAAC,CAAC,SAAA,CAAU,KAAA,CAAM,IAAA,EAAM,YAAY,CAAU,CAAA,GAC9C,EAAC;AAAA,IACP,CAAC;AAAA,GACH;AAEA,EAAA,OAAO,CAAC,MAAA,KAAW;AACjB,IAAA,IAAI,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,UAAA,EAAY;AAC7C,MAAA,MAAM,aAAA,GAAgB,gBAAA,CAAiB,GAAA,CAAI,MAAA,CAAO,IAAI,CAAA;AAEtD,MAAA,OAAO,aAAA,KAAkB,SACrB,MAAA,GACA,EAAE,eAAe,YAAA,EAAc,MAAA,EAAQ,aAAa,KAAA,EAAM;AAAA,IAChE;AAEA,IAAA,OAAO,MAAA,CAAO,SAAS,cAAA,CAAe,gBAAA,IACpC,CAAC,MAAA,CAAO,QAAA,IACR,MAAA,CAAO,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cACtC,cAAA,CAAe,GAAA,CAAI,OAAO,MAAA,CAAO,IAAI,KACrC,MAAA,CAAO,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,UAAA,GACtC;AAAA,MACE,aAAA,EAAe,OAAO,QAAA,CAAS,IAAA;AAAA,MAC/B,cAAc,MAAA,CAAO,QAAA;AAAA,MACrB,WAAA,EAAa;AAAA,KACf,GACA,MAAA;AAAA,EACN,CAAA;AACF;AAWO,MAAM,iBAAA,GAAoB,CAC/B,UAAA,EACA,IAAA,EACA,IAAA,KACY;AACZ,EAAA,KAAA,IACM,SAAA,GAAyC,WAAW,QAAA,CAAS,IAAI,GACrE,SAAA,KAAc,IAAA,EACd,SAAA,GAAY,SAAA,CAAU,KAAA,EACtB;AACA,IAAA,MAAM,QAAA,GAAW,SAAA,CAAU,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA;AAIvC,IAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,MAAA,OAAO,QAAA,CAAS,KAAK,IAAA,CAAK,CAAC,QAAQ,CAAC,qBAAA,CAAsB,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,IACrE;AAAA,EACF;AAEA,EAAA,OAAO,KAAA;AACT;AAWO,MAAM,eAAA,GAAkB,CAC7B,KAAA,EACA,OAAA,EACA,UAEA,KAAA,CAAM,MAAA,KAAW,CAAA,GACZ,EAAC,GACD;AAAA,EACC,KAAA,CAAM,gBAAA;AAAA,IACJ,OAAA;AAAA,IACA,MACG,GAAA,CAAI,CAAC,SAAS,CAAA,SAAA,EAAY,IAAI,YAAY,kBAAkB,CAAA;AAAA,CAAM,CAAA,CAClE,KAAK,EAAE;AAAA;AAEd;AAEN,MAAM,qBAAA,GAAwB,CAAC,IAAA,KAAA,CAC5B,IAAA,CAAK,SAAS,cAAA,CAAe,eAAA,IAC5B,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,IAC7B,KAAK,IAAA,KAAS,cAAA,CAAe,wBAAA,KAC/B,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,eAAe,iBAAA,IACpC,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,KAAA,KAAU,kBAAA;;;;"}
1
+ {"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.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 { TS_FORTRESS_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type ResolvedCallee = Readonly<{\n /** The canonical (imported) ts-fortress name, regardless of local alias. */\n canonicalName: string;\n /** The identifier node to rewrite when replacing the callee name. */\n propertyNode: TSESTree.Node;\n /** Whether the call was made through a namespace import (`t.foo(...)`). */\n isNamespace: boolean;\n}>;\n\n/** Every `import ... from 'ts-fortress'` declaration in the file. */\nexport const getTsFortressImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_FORTRESS_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-fortress export is imported as\n * (handling aliases such as `import { nonEmptyArray as nea }` → `'nea'`), or\n * `undefined` when it is not imported. Used so autofixes reference the binding\n * that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Builds a resolver that maps a call expression's callee to the canonical\n * ts-fortress function it refers to. Handles named imports (including aliases)\n * and namespace imports (`import * as t` → `t.foo(...)`) — the latter being the\n * idiomatic way to consume ts-fortress. Returns `undefined` for any callee that\n * does not reference the given ts-fortress imports.\n */\nexport const buildCalleeResolver = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n): ((callee: TSESTree.Expression) => ResolvedCallee | undefined) => {\n const specifiers = importDeclarations.flatMap(\n (declaration) => declaration.specifiers,\n );\n\n const namespaceNames = new Set(\n specifiers\n .filter(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,\n )\n .map((specifier) => specifier.local.name),\n );\n\n const localToCanonical = new Map<string, string>(\n specifiers.flatMap((specifier) => {\n if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) return [];\n\n const importedName =\n specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value;\n\n return typeof importedName === 'string'\n ? [[specifier.local.name, importedName] as const]\n : [];\n }),\n );\n\n return (callee) => {\n if (callee.type === AST_NODE_TYPES.Identifier) {\n const canonicalName = localToCanonical.get(callee.name);\n\n return canonicalName === undefined\n ? undefined\n : { canonicalName, propertyNode: callee, isNamespace: false };\n }\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n namespaceNames.has(callee.object.name) &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? {\n canonicalName: callee.property.name,\n propertyNode: callee.property,\n isNamespace: true,\n }\n : undefined;\n };\n};\n\n/**\n * Whether `name` resolves, at `node`, to a binding that is not a ts-fortress\n * import — a local declaration, a parameter, or an import from another module.\n * An autofix that makes a call reference `name` must bail out in that case,\n * because the rewritten call would resolve to the wrong binding.\n *\n * Scope-based rather than a scan of the top-level statements, so a binding that\n * merely shadows the ts-fortress one around the call site is caught too.\n */\nexport const hasForeignBinding = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n for (\n let mut_scope: TSESLint.Scope.Scope | null = sourceCode.getScope(node);\n mut_scope !== null;\n mut_scope = mut_scope.upper\n ) {\n const variable = mut_scope.set.get(name);\n\n // The innermost binding wins, so the first scope that declares `name`\n // decides — an outer ts-fortress import does not rescue a shadowed name.\n if (variable !== undefined) {\n return variable.defs.some((def) => !isTsFortressImportDef(def.node));\n }\n }\n\n return false;\n};\n\n/**\n * Inserts `import { <name> } from 'ts-fortress';` for each name at the top of\n * the file. Placement is deliberately naive: prettier-plugin-organize-imports\n * (or any equivalent) merges and sorts them afterwards.\n *\n * All names go into a single fix on purpose: ESLint merges the fixes of one\n * report into one edit span, so two reports each inserting at the start of the\n * file would conflict and one of them would be dropped.\n */\nexport const buildImportsFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n names: readonly string[],\n): readonly TSESLint.RuleFix[] =>\n Arr.isEmpty(names)\n ? ([] as const)\n : ([\n fixer.insertTextBefore(\n program,\n names\n .map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';\\n`)\n .join(''),\n ),\n ] as const);\n\nconst isTsFortressImportDef = (node: TSESTree.Node): boolean =>\n (node.type === AST_NODE_TYPES.ImportSpecifier ||\n node.type === AST_NODE_TYPES.ImportDefaultSpecifier ||\n node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) &&\n node.parent.type === AST_NODE_TYPES.ImportDeclaration &&\n node.parent.source.value === TS_FORTRESS_MODULE;\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-canonical-length-constrained-type.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-type.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport {\n ARRAY_FN,\n BOUNDED_LENGTH_TUPLE_FN,\n FIXED_LENGTH_TUPLE_FN,\n MAX_LENGTH_TUPLE_FN,\n MIN_LENGTH_ARRAY_FN,\n MIN_LENGTH_TUPLE_FN,\n NON_EMPTY_ARRAY_FN,\n STRUCTURAL_PREFIX_CAP,\n} from './constants.mjs';\nimport {\n buildCalleeResolver,\n buildImportsFix,\n getImportedLocalName,\n getTsFortressImports,\n hasForeignBinding,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useCanonicalType';\n\n/** How one leading length argument of a combinator call must look. */\ntype BoundMatcher = number | 'anyLength' | 'sameAsFirst';\n\ntype Rewrite = Readonly<{\n /** The canonical ts-fortress combinator the call resolves to. */\n from: string;\n /** One matcher per leading length argument the call must have. */\n bounds: readonly BoundMatcher[];\n /** The combinator the call is rewritten to. */\n to: string;\n /** Indices into `bounds` of the length arguments the rewrite keeps, in order. */\n keep: readonly number[];\n /**\n * Whether an options object carrying `defaultValue` blocks the rewrite,\n * because the two combinators type that option differently.\n */\n blockedByDefaultValue: boolean;\n}>;\n\n/**\n * Combinator calls whose length arguments make them a longhand spelling of\n * another combinator.\n *\n * Every entry is **type-identical**, not merely assignable — the two calls\n * produce the very same `Type<T>`, verified against the ts-type-forge\n * definitions:\n *\n * | call | canonical form | why |\n * | :---------------------------- | :------------------------ | :-- |\n * | `minLengthArray(1, x)` | `nonEmptyArray(x)` | `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` |\n * | `minLengthTuple(0, x)` | `array(x)` | `MinLengthTuple<0, A>` is `readonly A[]` — no constraint at all |\n * | `maxLengthTuple(0, x)` | `fixedLengthTuple(0, x)` | both are `readonly []` |\n * | `boundedLengthTuple(n, n, x)` | `fixedLengthTuple(n, x)` | the union collapses to its single longest member |\n * | `boundedLengthTuple(0, n, x)` | `maxLengthTuple(n, x)` | `MaxLengthTuple<N, A>` is defined as `BoundedLengthTuple<0, N, A>` |\n *\n * The rewrite also preserves `defaultValue` and every accepted/rejected value;\n * only the default `typeName` (and the `details.kind` of the length error it\n * reports) changes, to the one that names the constraint being checked.\n *\n * The `*Array` family has no analogous entries on purpose — see the README:\n * its length constraint lives in a brand, and `BoundedLengthArray<Min, Max, A>`\n * is an intersection of both bounds' brands, so neither\n * `boundedLengthArray(0, n, x)` → `maxLengthArray(n, x)` (which drops the\n * `MinLengthArray<0>` brand) nor `boundedLengthArray(n, n, x)` →\n * `fixedLengthArray(n, x)` (which adds an exact tuple for `n <= 10`) keeps the\n * type unchanged.\n */\nconst REWRITES = [\n {\n from: MIN_LENGTH_ARRAY_FN,\n bounds: [1],\n to: NON_EMPTY_ARRAY_FN,\n keep: [],\n blockedByDefaultValue: true,\n },\n {\n from: MIN_LENGTH_TUPLE_FN,\n bounds: [0],\n to: ARRAY_FN,\n keep: [],\n blockedByDefaultValue: false,\n },\n {\n from: MAX_LENGTH_TUPLE_FN,\n bounds: [0],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n // Listed before the `0, n` entry so that `boundedLengthTuple(0, 0, x)`\n // normalizes to `fixedLengthTuple(0, x)`, the more specific of the two.\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: ['anyLength', 'sameAsFirst'],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: [0, 'anyLength'],\n to: MAX_LENGTH_TUPLE_FN,\n keep: [1],\n blockedByDefaultValue: false,\n },\n] as const satisfies readonly Rewrite[];\n\nexport const preferCanonicalLengthConstrainedType: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useCanonicalType:\n 'Use `{{replacement}}(...)` instead of `{{original}}({{boundsText}}, ...)`: with these bounds both build exactly the same type, and `{{replacement}}` names the constraint directly (and reports it on validation failure).',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsFortressImports = getTsFortressImports(program);\n\n const resolveCallee = buildCalleeResolver(tsFortressImports);\n\n const mut_matches: {\n node: TSESTree.CallExpression;\n propertyNode: TSESTree.Node;\n isNamespace: boolean;\n rewrite: Rewrite;\n /** The name the rewritten call will use; `undefined` when unavailable. */\n localName: string | undefined;\n needsImport: boolean;\n }[] = [];\n\n return {\n CallExpression: (node) => {\n const resolved = resolveCallee(node.callee);\n\n if (resolved === undefined) return;\n\n const rewrite = REWRITES.find(\n (candidate) =>\n candidate.from === resolved.canonicalName &&\n matchesRewrite(node, candidate),\n );\n\n if (rewrite === undefined) return;\n\n // A namespace call (`t.maxLengthTuple(...)`) only swaps the property\n // name, so it never needs a binding of its own.\n if (resolved.isNamespace) {\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: true,\n rewrite,\n localName: rewrite.to,\n needsImport: false,\n });\n\n return;\n }\n\n const importedLocalName = getImportedLocalName(\n tsFortressImports,\n rewrite.to,\n );\n\n const localName = importedLocalName ?? rewrite.to;\n\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: false,\n rewrite,\n localName: hasForeignBinding(sourceCode, node, localName)\n ? undefined\n : localName,\n needsImport: importedLocalName === undefined,\n });\n },\n 'Program:exit': () => {\n const reportable = mut_matches.filter(\n (match) => match.localName !== undefined,\n );\n\n const namesNeedingImport = Array.from(\n new Set(\n reportable\n .filter((match) => match.needsImport)\n .map((match) => match.rewrite.to),\n ),\n );\n\n // Every missing import rides on a single report; see `buildImportsFix`.\n const importFixIndex = reportable.findIndex(\n (match) => match.needsImport,\n );\n\n for (const [\n index,\n { node, propertyNode, rewrite, localName },\n ] of reportable.entries()) {\n if (localName === undefined) continue;\n\n context.report({\n node,\n messageId: 'useCanonicalType',\n data: {\n original: rewrite.from,\n replacement: localName,\n boundsText: boundsText(node, rewrite, sourceCode),\n },\n fix: (fixer) => [\n ...(index === importFixIndex\n ? buildImportsFix(fixer, program, namesNeedingImport)\n : []),\n fixer.replaceText(propertyNode, localName),\n ...dropBoundsFix(fixer, node, rewrite),\n ],\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `node` is a `<combinator>(...bounds, elementType, options?)` call\n * whose bounds match `rewrite` exactly as written.\n */\nconst matchesRewrite = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): boolean => {\n const args = node.arguments;\n\n const boundCount = rewrite.bounds.length;\n\n if (\n args.length < boundCount + 1 ||\n args.length > boundCount + 2 ||\n // Explicit type arguments pin the bound independently of the literal, so\n // dropping or moving the literal could change the resulting type.\n node.typeArguments !== undefined ||\n args.some((arg) => arg.type === AST_NODE_TYPES.SpreadElement)\n ) {\n return false;\n }\n\n const bounds = args.slice(0, boundCount).map(getLengthLiteral);\n\n const [first] = bounds;\n\n const boundsMatch = rewrite.bounds.every((matcher, index) => {\n const value = bounds[index];\n\n if (value === undefined) return false;\n\n switch (matcher) {\n case 'anyLength':\n return value <= STRUCTURAL_PREFIX_CAP;\n\n case 'sameAsFirst':\n return value === first && value <= STRUCTURAL_PREFIX_CAP;\n\n default:\n return value === matcher;\n }\n });\n\n return (\n boundsMatch &&\n (!rewrite.blockedByDefaultValue ||\n !passesDefaultValue(args[boundCount + 1]))\n );\n};\n\n/** The bounds of the matched call, as written, for the report message. */\nconst boundsText = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n sourceCode: TSESLint.SourceCode,\n): string =>\n node.arguments\n .slice(0, rewrite.bounds.length)\n .map((arg) => sourceCode.getText(arg))\n .join(', ');\n\n/**\n * Removes the length arguments the rewrite drops. Each removal runs from the\n * dropped argument up to the start of the next one, so the comma, whitespace\n * and any comments in between go with it; the ranges never overlap.\n */\nconst dropBoundsFix = (\n fixer: TSESLint.RuleFixer,\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): readonly TSESLint.RuleFix[] =>\n rewrite.bounds.flatMap((_, index) => {\n if (rewrite.keep.includes(index)) return [];\n\n const dropped = node.arguments[index];\n\n const next = node.arguments[index + 1];\n\n return dropped === undefined || next === undefined\n ? []\n : [fixer.removeRange([dropped.range[0], next.range[0]])];\n });\n\n/**\n * A non-negative integer literal's value, or `undefined` for anything else —\n * a computed bound cannot be matched against the rewrite table.\n */\nconst getLengthLiteral = (node: TSESTree.Node): number | undefined =>\n node.type === AST_NODE_TYPES.Literal &&\n typeof node.value === 'number' &&\n Number.isSafeInteger(node.value) &&\n node.value >= 0\n ? node.value\n : undefined;\n\n/**\n * Whether the options argument may carry a `defaultValue`. Anything that is not\n * a plain object literal of statically known keys counts, since its contents\n * cannot be ruled out.\n */\nconst passesDefaultValue = (node: TSESTree.Node | undefined): boolean =>\n node !== undefined &&\n (node.type !== AST_NODE_TYPES.ObjectExpression ||\n node.properties.some(\n (property) =>\n property.type !== AST_NODE_TYPES.Property ||\n property.computed ||\n property.key.type !== AST_NODE_TYPES.Identifier ||\n property.key.name === 'defaultValue',\n ));\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-canonical-length-constrained-type.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-type.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport {\n ARRAY_FN,\n BOUNDED_LENGTH_TUPLE_FN,\n FIXED_LENGTH_TUPLE_FN,\n MAX_LENGTH_TUPLE_FN,\n MIN_LENGTH_ARRAY_FN,\n MIN_LENGTH_TUPLE_FN,\n NON_EMPTY_ARRAY_FN,\n STRUCTURAL_PREFIX_CAP,\n} from './constants.mjs';\nimport {\n buildCalleeResolver,\n buildImportsFix,\n getImportedLocalName,\n getTsFortressImports,\n hasForeignBinding,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useCanonicalType';\n\n/** How one leading length argument of a combinator call must look. */\ntype BoundMatcher = number | 'anyLength' | 'sameAsFirst';\n\ntype Rewrite = Readonly<{\n /** The canonical ts-fortress combinator the call resolves to. */\n from: string;\n /** One matcher per leading length argument the call must have. */\n bounds: readonly BoundMatcher[];\n /** The combinator the call is rewritten to. */\n to: string;\n /** Indices into `bounds` of the length arguments the rewrite keeps, in order. */\n keep: readonly number[];\n /**\n * Whether an options object carrying `defaultValue` blocks the rewrite,\n * because the two combinators type that option differently.\n */\n blockedByDefaultValue: boolean;\n}>;\n\n/**\n * Combinator calls whose length arguments make them a longhand spelling of\n * another combinator.\n *\n * Every entry is **type-identical**, not merely assignable — the two calls\n * produce the very same `Type<T>`, verified against the ts-type-forge\n * definitions:\n *\n * | call | canonical form | why |\n * | :---------------------------- | :------------------------ | :-- |\n * | `minLengthArray(1, x)` | `nonEmptyArray(x)` | `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` |\n * | `minLengthTuple(0, x)` | `array(x)` | `MinLengthTuple<0, A>` is `readonly A[]` — no constraint at all |\n * | `maxLengthTuple(0, x)` | `fixedLengthTuple(0, x)` | both are `readonly []` |\n * | `boundedLengthTuple(n, n, x)` | `fixedLengthTuple(n, x)` | the union collapses to its single longest member |\n * | `boundedLengthTuple(0, n, x)` | `maxLengthTuple(n, x)` | `MaxLengthTuple<N, A>` is defined as `BoundedLengthTuple<0, N, A>` |\n *\n * The rewrite also preserves `defaultValue` and every accepted/rejected value;\n * only the default `typeName` (and the `details.kind` of the length error it\n * reports) changes, to the one that names the constraint being checked.\n *\n * The `*Array` family has no analogous entries on purpose — see the README:\n * its length constraint lives in a brand, and `BoundedLengthArray<Min, Max, A>`\n * is an intersection of both bounds' brands, so neither\n * `boundedLengthArray(0, n, x)` → `maxLengthArray(n, x)` (which drops the\n * `MinLengthArray<0>` brand) nor `boundedLengthArray(n, n, x)` →\n * `fixedLengthArray(n, x)` (which adds an exact tuple for `n <= 10`) keeps the\n * type unchanged.\n */\nconst REWRITES = [\n {\n from: MIN_LENGTH_ARRAY_FN,\n bounds: [1],\n to: NON_EMPTY_ARRAY_FN,\n keep: [],\n blockedByDefaultValue: true,\n },\n {\n from: MIN_LENGTH_TUPLE_FN,\n bounds: [0],\n to: ARRAY_FN,\n keep: [],\n blockedByDefaultValue: false,\n },\n {\n from: MAX_LENGTH_TUPLE_FN,\n bounds: [0],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n // Listed before the `0, n` entry so that `boundedLengthTuple(0, 0, x)`\n // normalizes to `fixedLengthTuple(0, x)`, the more specific of the two.\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: ['anyLength', 'sameAsFirst'],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: [0, 'anyLength'],\n to: MAX_LENGTH_TUPLE_FN,\n keep: [1],\n blockedByDefaultValue: false,\n },\n] as const satisfies readonly Rewrite[];\n\nexport const preferCanonicalLengthConstrainedType: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useCanonicalType:\n 'Use `{{replacement}}(...)` instead of `{{original}}({{boundsText}}, ...)`: with these bounds both build exactly the same type, and `{{replacement}}` names the constraint directly (and reports it on validation failure).',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsFortressImports = getTsFortressImports(program);\n\n const resolveCallee = buildCalleeResolver(tsFortressImports);\n\n const mut_matches: {\n node: TSESTree.CallExpression;\n propertyNode: TSESTree.Node;\n isNamespace: boolean;\n rewrite: Rewrite;\n /** The name the rewritten call will use; `undefined` when unavailable. */\n localName: string | undefined;\n needsImport: boolean;\n }[] = [];\n\n return {\n CallExpression: (node) => {\n const resolved = resolveCallee(node.callee);\n\n if (resolved === undefined) return;\n\n const rewrite = REWRITES.find(\n (candidate) =>\n candidate.from === resolved.canonicalName &&\n matchesRewrite(node, candidate),\n );\n\n if (rewrite === undefined) return;\n\n // A namespace call (`t.maxLengthTuple(...)`) only swaps the property\n // name, so it never needs a binding of its own.\n if (resolved.isNamespace) {\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: true,\n rewrite,\n localName: rewrite.to,\n needsImport: false,\n });\n\n return;\n }\n\n const importedLocalName = getImportedLocalName(\n tsFortressImports,\n rewrite.to,\n );\n\n const localName = importedLocalName ?? rewrite.to;\n\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: false,\n rewrite,\n localName: hasForeignBinding(sourceCode, node, localName)\n ? undefined\n : localName,\n needsImport: importedLocalName === undefined,\n });\n },\n 'Program:exit': () => {\n const reportable = mut_matches.filter(\n (match) => match.localName !== undefined,\n );\n\n const namesNeedingImport = Array.from(\n new Set(\n reportable\n .filter((match) => match.needsImport)\n .map((match) => match.rewrite.to),\n ),\n );\n\n // Every missing import rides on a single report; see `buildImportsFix`.\n const importFixIndex = reportable.findIndex(\n (match) => match.needsImport,\n );\n\n for (const [\n index,\n { node, propertyNode, rewrite, localName },\n ] of reportable.entries()) {\n if (localName === undefined) continue;\n\n context.report({\n node,\n messageId: 'useCanonicalType',\n data: {\n original: rewrite.from,\n replacement: localName,\n boundsText: boundsText(node, rewrite, sourceCode),\n },\n fix: (fixer) => [\n ...(index === importFixIndex\n ? buildImportsFix(fixer, program, namesNeedingImport)\n : []),\n fixer.replaceText(propertyNode, localName),\n ...dropBoundsFix(fixer, node, rewrite),\n ],\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `node` is a `<combinator>(...bounds, elementType, options?)` call\n * whose bounds match `rewrite` exactly as written.\n */\nconst matchesRewrite = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): boolean => {\n const args = node.arguments;\n\n const boundCount = rewrite.bounds.length;\n\n if (\n args.length < boundCount + 1 ||\n args.length > boundCount + 2 ||\n // Explicit type arguments pin the bound independently of the literal, so\n // dropping or moving the literal could change the resulting type.\n node.typeArguments !== undefined ||\n args.some((arg) => arg.type === AST_NODE_TYPES.SpreadElement)\n ) {\n return false;\n }\n\n const bounds = args.slice(0, boundCount).map(getLengthLiteral);\n\n const [first] = bounds;\n\n const boundsMatch = rewrite.bounds.every((matcher, index) => {\n const value = bounds[index];\n\n if (value === undefined) return false;\n\n switch (matcher) {\n case 'anyLength':\n return value <= STRUCTURAL_PREFIX_CAP;\n\n case 'sameAsFirst':\n return value === first && value <= STRUCTURAL_PREFIX_CAP;\n\n default:\n return value === matcher;\n }\n });\n\n return (\n boundsMatch &&\n (!rewrite.blockedByDefaultValue ||\n !passesDefaultValue(args[boundCount + 1]))\n );\n};\n\n/** The bounds of the matched call, as written, for the report message. */\nconst boundsText = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n sourceCode: TSESLint.SourceCode,\n): string =>\n node.arguments\n .slice(0, rewrite.bounds.length)\n .map((arg) => sourceCode.getText(arg))\n .join(', ');\n\n/**\n * Removes the length arguments the rewrite drops. Each removal runs from the\n * dropped argument up to the start of the next one, so the comma, whitespace\n * and any comments in between go with it; the ranges never overlap.\n */\nconst dropBoundsFix = (\n fixer: TSESLint.RuleFixer,\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): readonly TSESLint.RuleFix[] =>\n rewrite.bounds.flatMap((_, index) => {\n if (rewrite.keep.includes(index)) return [];\n\n const dropped = node.arguments[index];\n\n const next = node.arguments[index + 1];\n\n return dropped === undefined || next === undefined\n ? []\n : [fixer.removeRange([dropped.range[0], next.range[0]])];\n });\n\n/**\n * A non-negative integer literal's value, or `undefined` for anything else —\n * a computed bound cannot be matched against the rewrite table.\n */\nconst getLengthLiteral = (node: TSESTree.Node): number | undefined =>\n node.type === AST_NODE_TYPES.Literal &&\n typeof node.value === 'number' &&\n Number.isSafeInteger(node.value) &&\n node.value >= 0\n ? node.value\n : undefined;\n\n/**\n * Whether the options argument may carry a `defaultValue`. Anything that is not\n * a plain object literal of statically known keys counts, since its contents\n * cannot be ruled out.\n */\nconst passesDefaultValue = (node: TSESTree.Node | undefined): boolean =>\n node !== undefined &&\n (node.type !== AST_NODE_TYPES.ObjectExpression ||\n node.properties.some(\n (property) =>\n property.type !== AST_NODE_TYPES.Property ||\n property.computed ||\n property.key.type !== AST_NODE_TYPES.Identifier ||\n property.key.name === 'defaultValue',\n ));\n"],"names":[],"mappings":"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"}
package/dist/types.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type TSESLint } from '@typescript-eslint/utils';
2
2
  import { type DeepReadonly } from 'ts-type-forge';
3
3
  export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
4
+ export type ESLintFlatConfig = DeepReadonly<TSESLint.FlatConfig.Config>;
4
5
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-fortress",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer schema definitions toward ts-fortress idioms.",
6
6
  "keywords": [
@@ -36,7 +36,8 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@typescript-eslint/utils": "8.65.0",
39
- "ts-type-forge": "7.x"
39
+ "ts-data-forge": "~14.1.0",
40
+ "ts-type-forge": "~9.1.3"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@typescript-eslint/parser": "8.65.0",
package/src/index.mts CHANGED
@@ -4,4 +4,4 @@ export type {
4
4
  EslintTsFortressRulesOption,
5
5
  } from './rule-types.mjs';
6
6
  export { tsFortressRules } from './rules/index.mjs';
7
- export { type ESLintPlugin } from './types.mjs';
7
+ export { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
package/src/plugin.mts CHANGED
@@ -1,9 +1,43 @@
1
+ import { type ReadonlyRecord } from 'ts-type-forge';
1
2
  import { tsFortressRules } from './rules/index.mjs';
2
- import { type ESLintPlugin } from './types.mjs';
3
+ import { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
3
4
 
4
- export const eslintPluginTsFortress: ESLintPlugin = {
5
+ /**
6
+ * Every rule this plugin ships, at `error`.
7
+ *
8
+ * The `satisfies` clause below is keyed off {@link tsFortressRules}, so adding
9
+ * a rule without listing it here fails to type-check.
10
+ */
11
+ const recommendedRules = {
12
+ 'ts-fortress/prefer-canonical-length-constrained-type': 'error',
13
+ } as const satisfies ReadonlyRecord<
14
+ `ts-fortress/${keyof typeof tsFortressRules}`,
15
+ 'error'
16
+ >;
17
+
18
+ const recommendedConfig = {
19
+ name: 'ts-fortress/recommended',
20
+ plugins: {
21
+ // Resolved lazily so that this config registers the *same* object that
22
+ // `eslintPluginTsFortress` refers to. ESLint rejects a plugin name that
23
+ // maps to two different objects with `Cannot redefine plugin`, which is
24
+ // what a user combining this preset with their own
25
+ // `plugins: { 'ts-fortress': eslintPluginTsFortress }` entry would hit if
26
+ // the preset embedded a separate copy of the plugin.
27
+ get 'ts-fortress'(): ESLintPlugin {
28
+ return eslintPluginTsFortress;
29
+ },
30
+ },
31
+ rules: recommendedRules,
32
+ } as const satisfies ESLintFlatConfig;
33
+
34
+ export const eslintPluginTsFortress = {
5
35
  meta: {
6
36
  name: 'eslint-plugin-ts-fortress',
7
37
  },
8
38
  rules: tsFortressRules,
9
- } as const;
39
+ configs: {
40
+ /** Enables every rule of this plugin at `error`. */
41
+ recommended: recommendedConfig,
42
+ },
43
+ } as const satisfies ESLintPlugin;
@@ -0,0 +1,24 @@
1
+ import { eslintPluginTsFortress } from './plugin.mjs';
2
+ import { tsFortressRules } from './rules/index.mjs';
3
+
4
+ describe('eslintPluginTsFortress.configs.recommended', () => {
5
+ const recommended = eslintPluginTsFortress.configs.recommended;
6
+
7
+ test('enables every rule of the plugin, and nothing else, at "error"', () => {
8
+ assert.deepStrictEqual(
9
+ recommended.rules,
10
+ Object.fromEntries(
11
+ Object.keys(tsFortressRules).map((name) => [
12
+ `ts-fortress/${name}`,
13
+ 'error',
14
+ ]),
15
+ ),
16
+ );
17
+ });
18
+
19
+ test('registers the exported plugin object itself', () => {
20
+ // Registering a *copy* would make `Cannot redefine plugin` errors possible
21
+ // for users who also list the plugin in their own `plugins` record.
22
+ expect(recommended.plugins['ts-fortress']).toBe(eslintPluginTsFortress);
23
+ });
24
+ });
@@ -4,7 +4,7 @@
4
4
  // (`scripts/cmd/gen-rule-types.mts`). Do not edit it manually.
5
5
 
6
6
  import { type Linter } from 'eslint';
7
- import { type TypeEq } from 'ts-type-forge';
7
+ import { type ReadonlyRecord, type TypeEq } from 'ts-type-forge';
8
8
  import { type tsFortressRules } from './rules/index.mjs';
9
9
 
10
10
  /**
@@ -26,7 +26,7 @@ export type EslintTsFortressRules = Readonly<{
26
26
  'ts-fortress/prefer-canonical-length-constrained-type': PreferCanonicalLengthConstrainedType.RuleEntry;
27
27
  }>;
28
28
 
29
- export type EslintTsFortressRulesOption = Readonly<Record<never, never>>;
29
+ export type EslintTsFortressRulesOption = ReadonlyRecord<never, never>;
30
30
 
31
31
  // If this assertion fails to type-check, this generated file has drifted from
32
32
  // the actual plugin rules — re-run `pnpm run gen:rule-types`.
@@ -3,6 +3,7 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
+ import { Arr } from 'ts-data-forge';
6
7
  import { TS_FORTRESS_MODULE } from './constants.mjs';
7
8
 
8
9
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
@@ -152,7 +153,7 @@ export const buildImportsFix = (
152
153
  program: TSESTree.Program,
153
154
  names: readonly string[],
154
155
  ): readonly TSESLint.RuleFix[] =>
155
- names.length === 0
156
+ Arr.isEmpty(names)
156
157
  ? ([] as const)
157
158
  : ([
158
159
  fixer.insertTextBefore(
@@ -169,5 +170,3 @@ const isTsFortressImportDef = (node: TSESTree.Node): boolean =>
169
170
  node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) &&
170
171
  node.parent.type === AST_NODE_TYPES.ImportDeclaration &&
171
172
  node.parent.source.value === TS_FORTRESS_MODULE;
172
-
173
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -356,5 +356,3 @@ const passesDefaultValue = (node: TSESTree.Node | undefined): boolean =>
356
356
  property.key.type !== AST_NODE_TYPES.Identifier ||
357
357
  property.key.name === 'defaultValue',
358
358
  ));
359
-
360
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
package/src/types.mts CHANGED
@@ -2,3 +2,5 @@ import { type TSESLint } from '@typescript-eslint/utils';
2
2
  import { type DeepReadonly } from 'ts-type-forge';
3
3
 
4
4
  export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
5
+
6
+ export type ESLintFlatConfig = DeepReadonly<TSESLint.FlatConfig.Config>;