@ttsc/lint 0.24.0 → 0.26.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/src/index.ts CHANGED
@@ -8,9 +8,7 @@ import path from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
9
 
10
10
  import {
11
- CONFIG_EVALUATOR_PROCESS_OPTIONS,
12
- CONFIG_EVALUATOR_STATUS_FD,
13
- configEvaluatorBoundaryEnvironment,
11
+ configEvaluatorFailureReason,
14
12
  configEvaluatorProcessFailure,
15
13
  } from "./internal/configEvaluatorFailure";
16
14
  import type { ITtscLintPlugin, ITtscLintPluginConfig } from "./structures";
@@ -630,33 +628,68 @@ const hooks = registerHooks({
630
628
  },
631
629
  });
632
630
 
633
- try {
634
- const importedConfig = configLocation.toLowerCase().endsWith(".json")
635
- ? JSON.parse(fs.readFileSync(configLocation, "utf8").replace(/^\uFEFF/, ""))
636
- : await import(configUrl);
637
- const current = await resolveConfig(importedConfig, true);
638
- const pluginMaps = collectPluginObjects(current);
639
- const entries: Array<{ namespace: string; source: string }> = [];
640
- for (const map of pluginMaps) {
641
- for (const [namespace, value] of Object.entries(map)) {
642
- const source = extractPluginSource(value);
643
- if (source === undefined || source.length === 0) {
644
- throw new Error(
645
- \`contributor \${JSON.stringify(namespace)} must resolve to an object with a non-empty "source" string\`,
646
- );
631
+ // Wrapped and settled explicitly rather than written as a top-level await.
632
+ // The loader tsconfig's "module" now follows the config's own package, and
633
+ // TS1378 rejects top-level await under a CommonJS module option however this
634
+ // .mts file emits. The trailing catch is what a top-level await gave for
635
+ // free: without it a throw from the finally would leave the promise
636
+ // unsettled instead of failing the load.
637
+ (async () => {
638
+ try {
639
+ const importedConfig = configLocation.toLowerCase().endsWith(".json")
640
+ ? JSON.parse(fs.readFileSync(configLocation, "utf8").replace(/^\uFEFF/, ""))
641
+ : await import(configUrl);
642
+ const current = await resolveConfig(importedConfig, true);
643
+ const pluginMaps = collectPluginObjects(current);
644
+ const entries: Array<{ namespace: string; source: string }> = [];
645
+ for (const map of pluginMaps) {
646
+ for (const [namespace, value] of Object.entries(map)) {
647
+ const source = extractPluginSource(value);
648
+ if (source === undefined || source.length === 0) {
649
+ throw new Error(
650
+ \`contributor \${JSON.stringify(namespace)} must resolve to an object with a non-empty "source" string\`,
651
+ );
652
+ }
653
+ entries.push({ namespace, source });
647
654
  }
648
- entries.push({ namespace, source });
649
655
  }
656
+ fs.writeFileSync(outputPath, JSON.stringify({
657
+ dependencies: finalizeDependencies(),
658
+ entries,
659
+ }), "utf8");
660
+ } catch (error) {
661
+ reportLoaderFailure(error);
662
+ } finally {
663
+ hooks.deregister();
650
664
  }
651
- fs.writeFileSync(outputPath, JSON.stringify({
652
- dependencies: finalizeDependencies(),
653
- entries,
654
- }), "utf8");
655
- } catch (error) {
656
- process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
657
- process.exit(1);
658
- } finally {
659
- hooks.deregister();
665
+ })().catch((error) => {
666
+ // Reached only when the finally above throws: the catch already ends the
667
+ // process, so this is the deregistration's own failure, not the config's.
668
+ reportLoaderFailure(error);
669
+ });
670
+
671
+ // reportLoaderFailure ends this loader on an error it can name, on both of the
672
+ // channels the parent uses.
673
+ //
674
+ // The stack is for a reader and streams to stderr as it is written. The reason
675
+ // is a fact about the user's config that a caller has to act on, so it travels
676
+ // as data through the result file the parent already reads. Only a well-formed
677
+ // envelope is honoured there, so a partially written or unrelated file leaves
678
+ // the process status to speak for itself.
679
+ function reportLoaderFailure(error: unknown): never {
680
+ // The trailing newline ends the stack as a line of its own. Without it the
681
+ // parent's own message, written to this same stream, starts mid-line.
682
+ process.stderr.write((error instanceof Error && error.stack ? error.stack : String(error)) + "\\n");
683
+ try {
684
+ fs.writeFileSync(
685
+ outputPath,
686
+ JSON.stringify({ __ttscLoaderError: error instanceof Error ? error.message : String(error) }),
687
+ "utf8",
688
+ );
689
+ } catch {
690
+ // A reason that cannot be written leaves the exit status as the report.
691
+ }
692
+ return process.exit(1);
660
693
  }
661
694
 
662
695
  function isObject(value: unknown): value is Record<string, unknown> {
@@ -1702,7 +1735,13 @@ function evaluateTtsxConfigPlugins(
1702
1735
  allowImportingTsExtensions: true,
1703
1736
  allowJs: true,
1704
1737
  checkJs: false,
1705
- module: "ESNext",
1738
+ // The config is a Node module, so Node's rule decides its format:
1739
+ // the nearest `package.json` `type` above it. Hardcoding one answer
1740
+ // ran every ambiguous `.ts` config as ESM and broke `__dirname` in
1741
+ // an ordinary CommonJS package (#1068). `moduleResolution` stays
1742
+ // `bundler`, which tsgo accepts for both kinds, so extensionless
1743
+ // relative imports keep resolving either way.
1744
+ module: configModuleOption(configPath),
1706
1745
  moduleResolution: "bundler",
1707
1746
  noImplicitAny: false,
1708
1747
  outDir: path.join(tempDir, "out").replace(/\\/g, "/"),
@@ -1711,6 +1750,12 @@ function evaluateTtsxConfigPlugins(
1711
1750
  skipLibCheck: true,
1712
1751
  strict: false,
1713
1752
  target: "ES2022",
1753
+ // TypeScript 7 includes no ambient type package unless `types` asks
1754
+ // for it, and this Program extends nothing, so without the wildcard
1755
+ // a config could not name a single Node global (#1068). The loader
1756
+ // directory links the config's nearest `node_modules`, so the
1757
+ // default `typeRoots` walk finds exactly what the project installed.
1758
+ types: ["*"],
1714
1759
  },
1715
1760
  files: [
1716
1761
  loaderPath.replace(/\\/g, "/"),
@@ -1749,28 +1794,29 @@ function evaluateTtsxConfigPlugins(
1749
1794
  }
1750
1795
  const env = {
1751
1796
  ...nodeConfigLoaderEnv(configPath),
1752
- ...configEvaluatorBoundaryEnvironment(),
1753
1797
  };
1754
1798
  const command = ttsxThroughNodeIfNeeded(ttsxBinary);
1755
1799
  const result = spawnSync(command.binary, [...command.prefix, ...args], {
1756
1800
  cwd: tempDir,
1757
1801
  env,
1758
- encoding: "utf8",
1759
- ...CONFIG_EVALUATOR_PROCESS_OPTIONS,
1760
- stdio: [
1761
- "ignore",
1762
- "pipe",
1763
- "pipe",
1764
- ...Array.from(
1765
- { length: CONFIG_EVALUATOR_STATUS_FD - 2 },
1766
- () => "pipe" as const,
1767
- ),
1768
- ],
1802
+ // Both child streams are human output, and they go straight to this
1803
+ // process's stderr as they are written. Nothing is collected here: the
1804
+ // parent's stdout is reserved for compiler JSON and LSP frames, and
1805
+ // buffering the child only to replay it afterwards is what forced an
1806
+ // invented output ceiling and made a long evaluation print nothing at all.
1807
+ stdio: ["ignore", 2, 2],
1769
1808
  windowsHide: true,
1770
1809
  });
1771
- forwardConfigEvaluatorStreams(result.stdout, result.stderr);
1772
1810
  const processFailure = configEvaluatorProcessFailure(result, configPath);
1773
- if (processFailure) throw processFailure;
1811
+ if (processFailure) {
1812
+ // The evaluator's stack already reached the user's stderr as it ran. What
1813
+ // it could not put there is a reason a caller can act on, so that arrives
1814
+ // through the result file instead.
1815
+ const reason = configEvaluatorFailureReason(outputPath);
1816
+ throw reason === ""
1817
+ ? processFailure
1818
+ : new Error(`${processFailure.message}\n${reason}`);
1819
+ }
1774
1820
  let payload: {
1775
1821
  dependencies?: ConfigDependencyFingerprint[];
1776
1822
  entries?: ConfigPluginEntry[];
@@ -1841,20 +1887,10 @@ function evaluateTtsxConfigPlugins(
1841
1887
  }
1842
1888
  return { dependencies, entries };
1843
1889
  } finally {
1844
- fs.rmSync(tempDir, { recursive: true, force: true });
1890
+ removeEvaluationTempDir(tempDir);
1845
1891
  }
1846
1892
  }
1847
1893
 
1848
- function forwardConfigEvaluatorStreams(
1849
- stdout: string | null | undefined,
1850
- stderr: string | null | undefined,
1851
- ): void {
1852
- // Both child streams are human output. Parent stdout is reserved for compiler
1853
- // JSON or LSP frames, so even a user console.log is redirected.
1854
- if (stdout) process.stderr.write(stdout);
1855
- if (stderr) process.stderr.write(stderr);
1856
- }
1857
-
1858
1894
  // ────────────────────────────────────────────────────────────────────────────
1859
1895
  // Config cache (shared with the Go sidecar — packages/lint/linthost/config.go)
1860
1896
  // ────────────────────────────────────────────────────────────────────────────
@@ -2268,6 +2304,64 @@ function findNearestNodeModules(start: string): string | undefined {
2268
2304
  }
2269
2305
  }
2270
2306
 
2307
+ /**
2308
+ * The loader tsconfig's `module` for a given config file: the module kind Node
2309
+ * would give the file itself.
2310
+ *
2311
+ * An explicit `.cts`/`.cjs` or `.mts`/`.mjs` extension already decides the emit
2312
+ * format on its own, so those keep the ES-module setting and let the extension
2313
+ * win — the same precedence tsgo applies. Everything ambiguous walks up for the
2314
+ * nearest `package.json` `type`, exactly as Node does when it loads the file.
2315
+ */
2316
+ function configModuleOption(configPath: string): string {
2317
+ const extension = path.extname(configPath).toLowerCase();
2318
+ if (extension !== ".ts" && extension !== ".tsx" && extension !== ".js") {
2319
+ return "ESNext";
2320
+ }
2321
+ return nearestPackageType(configPath) === "commonjs" ? "CommonJS" : "ESNext";
2322
+ }
2323
+
2324
+ /**
2325
+ * Nearest `package.json` `"type"` at or above `configPath`, mirroring Node's
2326
+ * package-scope lookup: the walk stops at the **first** manifest it finds, and
2327
+ * a manifest that declares no `"type"` means CommonJS rather than a reason to
2328
+ * keep climbing. Reaching the filesystem root without any manifest also means
2329
+ * CommonJS.
2330
+ */
2331
+ function nearestPackageType(configPath: string): "commonjs" | "module" {
2332
+ let dir = path.dirname(path.resolve(configPath));
2333
+ for (;;) {
2334
+ // One read rather than a stat-then-read: an unreadable entry and a missing
2335
+ // one are the same answer here — keep walking — and the Go loaders resolve
2336
+ // it the same way, so the four implementations of this rule cannot drift.
2337
+ let raw: string | undefined;
2338
+ try {
2339
+ raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
2340
+ } catch {
2341
+ raw = undefined;
2342
+ }
2343
+ if (raw !== undefined) {
2344
+ try {
2345
+ const manifest: unknown = JSON.parse(raw);
2346
+ const type =
2347
+ typeof manifest === "object" && manifest !== null
2348
+ ? (manifest as { type?: unknown }).type
2349
+ : undefined;
2350
+ return type === "module" ? "module" : "commonjs";
2351
+ } catch {
2352
+ // A manifest that does not parse still bounds the package scope; Node
2353
+ // refuses to look past it, and CommonJS is the format it defaults to.
2354
+ return "commonjs";
2355
+ }
2356
+ }
2357
+ const parent = path.dirname(dir);
2358
+ if (parent === dir) {
2359
+ return "commonjs";
2360
+ }
2361
+ dir = parent;
2362
+ }
2363
+ }
2364
+
2271
2365
  function linkNearestNodeModules(tempDir: string, sourceDir: string): void {
2272
2366
  const nodeModules = findNearestNodeModules(sourceDir);
2273
2367
  if (!nodeModules) return;
@@ -2400,3 +2494,19 @@ function ttsxThroughNodeIfNeeded(binary: string): {
2400
2494
  }
2401
2495
  return { binary, prefix: [] };
2402
2496
  }
2497
+
2498
+ /**
2499
+ * Remove an evaluation temp directory without letting cleanup replace a result.
2500
+ *
2501
+ * This runs from a `finally`, so a throw here would surface instead of the
2502
+ * evaluation's own outcome — and on Windows a grandchild that inherited a
2503
+ * handle, or a scanner holding the file, can make removal fail. Leaving bytes
2504
+ * in the system temp directory is by far the lesser outcome.
2505
+ */
2506
+ function removeEvaluationTempDir(directory: string): void {
2507
+ try {
2508
+ fs.rmSync(directory, { force: true, recursive: true });
2509
+ } catch {
2510
+ // Best effort.
2511
+ }
2512
+ }
@@ -1,51 +1,31 @@
1
- export const CONFIG_EVALUATOR_MAX_BUFFER = 16 * 1024 * 1024;
2
- export const CONFIG_EVALUATOR_TIMEOUT_MS = 60_000;
3
- export const CONFIG_EVALUATOR_TEARDOWN_GRACE_MS = 5_000;
4
- export const CONFIG_EVALUATOR_STATUS_FD = 3;
5
- export const CONFIG_EVALUATOR_PROCESS_OPTIONS = Object.freeze({
6
- killSignal: "SIGKILL" as const,
7
- maxBuffer: CONFIG_EVALUATOR_MAX_BUFFER,
8
- timeout: CONFIG_EVALUATOR_TIMEOUT_MS + CONFIG_EVALUATOR_TEARDOWN_GRACE_MS,
9
- });
1
+ import fs from "node:fs";
10
2
 
11
3
  interface ConfigEvaluatorProcessResult {
12
4
  error?: Error;
13
- output?: readonly (string | null)[] | null;
14
5
  signal: NodeJS.Signals | null;
15
6
  status: number | null;
16
- stderr: string | null | undefined;
17
7
  }
18
8
 
19
9
  /**
20
10
  * Classify the ways the isolated lint-config evaluator can stop.
21
11
  *
22
- * Node reports both timeout and max-buffer termination with the configured
23
- * signal, so the process error code must take precedence over the signal. The
24
- * evaluator uses `SIGKILL`: Node's synchronous process API otherwise keeps
25
- * waiting when a POSIX child handles the default `SIGTERM` without exiting. A
26
- * bare signal is an external termination and a non-zero status is an evaluator
27
- * failure whose stderr tail contains the useful user-config diagnostic.
12
+ * The evaluator writes the child's own output straight to this process's
13
+ * stderr, so a diagnostic has already reached the user by the time anything
14
+ * here runs. What is left to say is only how the process ended: it never
15
+ * launched, something outside killed it, or it exited non-zero after printing
16
+ * its own reason.
17
+ *
18
+ * Nothing is bounded here — not time, not output. Both were the compiler
19
+ * deciding, on numbers nobody chose for this machine, that a user's own config
20
+ * had run too long or said too much. A slow config is a slow build the user can
21
+ * watch and interrupt; a loud one is output they asked for. Neither is this
22
+ * process's memory to spend either, because the child's streams are no longer
23
+ * collected into it.
28
24
  */
29
25
  export function configEvaluatorProcessFailure(
30
26
  result: ConfigEvaluatorProcessResult,
31
27
  configPath: string,
32
28
  ): Error | undefined {
33
- const code = (result.error as NodeJS.ErrnoException | undefined)?.code;
34
- const nestedCode = result.output?.[CONFIG_EVALUATOR_STATUS_FD]?.trim() ?? "";
35
- if (code === "ETIMEDOUT" || nestedCode === "ETIMEDOUT") {
36
- return new Error(
37
- `@ttsc/lint: ttsx evaluation of ${configPath} timed out after ` +
38
- `${CONFIG_EVALUATOR_TIMEOUT_MS / 1_000} seconds. ` +
39
- "Simplify the config or move heavy work out of top-level.",
40
- );
41
- }
42
- if (code === "ENOBUFS" || nestedCode === "ENOBUFS") {
43
- return new Error(
44
- `@ttsc/lint: ttsx evaluation of ${configPath} exceeded the ` +
45
- `${CONFIG_EVALUATOR_MAX_BUFFER / (1024 * 1024)} MiB output limit. ` +
46
- "Reduce console output from the config and its dependencies.",
47
- );
48
- }
49
29
  if (result.error) {
50
30
  return new Error(
51
31
  `@ttsc/lint: failed to spawn ttsx for ${configPath}: ${result.error.message}`,
@@ -57,43 +37,34 @@ export function configEvaluatorProcessFailure(
57
37
  );
58
38
  }
59
39
  if (result.status !== 0) {
60
- const reason = configEvaluatorFailureReason(result.stderr);
61
40
  return new Error(
62
- `@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}` +
63
- (reason === "" ? "" : "\n" + reason),
41
+ `@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}`,
64
42
  );
65
43
  }
66
44
  return undefined;
67
45
  }
68
46
 
69
47
  /**
70
- * Pass the semantic deadline and private status pipe through the `ttsx` wrapper
71
- * to the runtime child that actually executes the config.
72
- */
73
- export function configEvaluatorBoundaryEnvironment(
74
- now: number = Date.now(),
75
- ): NodeJS.ProcessEnv {
76
- return {
77
- TTSC_TTSX_EVALUATOR_DEADLINE_MS: String(now + CONFIG_EVALUATOR_TIMEOUT_MS),
78
- TTSC_TTSX_EVALUATOR_MAX_BUFFER_BYTES: String(CONFIG_EVALUATOR_MAX_BUFFER),
79
- TTSC_TTSX_EVALUATOR_STATUS_FD: String(CONFIG_EVALUATOR_STATUS_FD),
80
- };
81
- }
82
-
83
- /**
84
- * Return the useful tail of evaluator stderr without turning an exception into
85
- * an unbounded duplicate of the already-forwarded child stream.
48
+ * Read the failure envelope the evaluator writes to its result file when it
49
+ * stops on an error it can name.
50
+ *
51
+ * This is the other half of classifying how the evaluation ended, which is why
52
+ * it lives beside {@link configEvaluatorProcessFailure} rather than at the call
53
+ * site: the status says that it failed, and this says why.
54
+ *
55
+ * Only a well-formed envelope is honoured. A real evaluation payload never
56
+ * carries this key, and every other shape — an absent file, a build that failed
57
+ * before the loader ran, a half-written result, a payload written before a
58
+ * later non-zero exit — leaves the process status to speak for itself.
86
59
  */
87
- function configEvaluatorFailureReason(
88
- stderr: string | null | undefined,
89
- ): string {
90
- const bounded = (stderr ?? "").slice(-CONFIG_EVALUATOR_REASON_MAX_CHARS);
91
- const lines = bounded
92
- .split(/\r?\n/)
93
- .map((line) => line.trimEnd())
94
- .filter((line) => line.trim() !== "");
95
- return lines.slice(-CONFIG_EVALUATOR_REASON_LINES).join("\n");
60
+ export function configEvaluatorFailureReason(outputPath: string): string {
61
+ try {
62
+ const parsed: unknown = JSON.parse(fs.readFileSync(outputPath, "utf8"));
63
+ if (typeof parsed !== "object" || parsed === null) return "";
64
+ const message = (parsed as { __ttscLoaderError?: unknown })
65
+ .__ttscLoaderError;
66
+ return typeof message === "string" ? message.trim() : "";
67
+ } catch {
68
+ return "";
69
+ }
96
70
  }
97
-
98
- const CONFIG_EVALUATOR_REASON_LINES = 5;
99
- const CONFIG_EVALUATOR_REASON_MAX_CHARS = 8 * 1024;
@@ -80,24 +80,45 @@ export interface ITtscLintFunctionalNoThrowStatementsRuleOptions {
80
80
 
81
81
  /** `functional/no-mixed-types` rule options. */
82
82
  export interface ITtscLintFunctionalNoMixedTypesRuleOptions {
83
- /** Check interface member kinds. */
83
+ /**
84
+ * Check interface member kinds.
85
+ *
86
+ * @default true
87
+ */
84
88
  checkInterfaces?: boolean;
85
89
 
86
- /** Check type-literal member kinds. */
90
+ /**
91
+ * Check type-literal member kinds.
92
+ *
93
+ * @default true
94
+ */
87
95
  checkTypeLiterals?: boolean;
88
96
  }
89
97
 
90
98
  /** `functional/no-return-void` rule options. */
91
99
  export interface ITtscLintFunctionalNoReturnVoidRuleOptions {
92
- /** Permit a function that returns `null` to satisfy the rule. */
100
+ /**
101
+ * Permit a function whose declared return type is `null`. Set `false` to
102
+ * reject it the way a declared `void` is rejected.
103
+ *
104
+ * @default true
105
+ */
93
106
  allowNull?: boolean;
94
107
 
95
- /** Permit a function that returns `undefined` to satisfy the rule. */
108
+ /**
109
+ * Permit a function whose declared return type is `undefined`. Set `false` to
110
+ * reject it the way a declared `void` is rejected.
111
+ *
112
+ * @default true
113
+ */
96
114
  allowUndefined?: boolean;
97
115
 
98
116
  /**
99
- * Skip functions whose return type is inferred to be `void` rather than
100
- * declared explicitly.
117
+ * Skip a bare `return;` inside a function that declares no return type. That
118
+ * statement is the one place the rule rejects a void-ness it inferred rather
119
+ * than read from an annotation.
120
+ *
121
+ * @default false
101
122
  */
102
123
  ignoreInferredTypes?: boolean;
103
124
  }
@@ -105,7 +126,8 @@ export interface ITtscLintFunctionalNoReturnVoidRuleOptions {
105
126
  /** `functional/prefer-immutable-types` rule options. */
106
127
  export interface ITtscLintFunctionalPreferImmutableTypesRuleOptions extends ITtscLintFunctionalPatternOptions {
107
128
  /**
108
- * Minimum accepted immutability. The native subset treats any configured
129
+ * Minimum accepted immutability. Reserved for upstream-compatible configs;
130
+ * the native subset computes no immutability level and treats any configured
109
131
  * value as readonly-required.
110
132
  */
111
133
  enforcement?:
@@ -118,31 +140,62 @@ export interface ITtscLintFunctionalPreferImmutableTypesRuleOptions extends ITts
118
140
 
119
141
  /** `functional/prefer-readonly-type` rule options. */
120
142
  export interface ITtscLintFunctionalPreferReadonlyTypeRuleOptions extends ITtscLintFunctionalPatternOptions {
121
- /** Permit mutation of locals while still policing exported types. */
143
+ /**
144
+ * Permit mutation of locals while still policing exported types. Reserved for
145
+ * upstream-compatible configs; the native subset reads type annotations and
146
+ * models no local-versus-exported distinction.
147
+ */
122
148
  allowLocalMutation?: boolean;
123
149
 
124
- /** Permit a mutable return type even when parameters must be readonly. */
150
+ /**
151
+ * Permit a mutable return type even when parameters must be readonly. Covers
152
+ * every signature that can declare one, including call and construct
153
+ * signatures, constructor types, and a get accessor.
154
+ *
155
+ * @default false
156
+ */
125
157
  allowMutableReturnType?: boolean;
126
158
 
127
- /** Also check property positions that have no explicit type annotation. */
159
+ /**
160
+ * Also check property positions that have no explicit type annotation.
161
+ * Reserved for upstream-compatible configs; judging an unannotated position
162
+ * needs the type checker, which this rule does not use.
163
+ */
128
164
  checkImplicit?: boolean;
129
165
 
130
- /** Skip array / tuple / `Map` / `Set` types. */
166
+ /**
167
+ * Skip array / tuple / `Map` / `Set` types.
168
+ *
169
+ * @default false
170
+ */
131
171
  ignoreCollections?: boolean;
132
172
 
133
173
  /**
134
- * Skip class fields. `"fieldsOnly"` keeps the rule active for non-field class
135
- * members.
174
+ * Skip class members. `true` skips anything under a class, its heritage
175
+ * clause and type parameters included; `"fieldsOnly"` narrows that to field
176
+ * declarations and keeps methods, accessors, and constructor parameters
177
+ * checked.
178
+ *
179
+ * @default false
136
180
  */
137
181
  ignoreClass?: boolean | "fieldsOnly";
138
182
 
139
- /** Skip interface members entirely. */
183
+ /**
184
+ * Skip interface members entirely.
185
+ *
186
+ * @default false
187
+ */
140
188
  ignoreInterface?: boolean;
141
189
  }
142
190
 
143
191
  /** `functional/prefer-tacit` rule options. */
144
192
  export interface ITtscLintFunctionalPreferTacitRuleOptions {
145
- /** Check member expressions such as `x => service.map(x)`. */
193
+ /**
194
+ * Check member expressions such as `x => service.map(x)`. Set `false` to keep
195
+ * the rule on bare-identifier callees only.
196
+ *
197
+ * @default true
198
+ */
146
199
  checkMemberExpressions?: boolean;
147
200
  }
148
201
 
@@ -167,7 +220,11 @@ export interface ITtscLintFunctionalTypeDeclarationImmutabilityRule {
167
220
  */
168
221
  immutability?: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable" | "Mutable";
169
222
 
170
- /** Comparator applied to the immutability level above. */
223
+ /**
224
+ * Comparator applied to the immutability level above. Reserved for
225
+ * upstream-compatible configs alongside `immutability`: the native subset
226
+ * computes no immutability level, so there is nothing to compare.
227
+ */
171
228
  comparator?:
172
229
  | "Less"
173
230
  | "AtMost"