@ttsc/lint 0.15.2 → 0.15.4

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
@@ -1132,6 +1132,8 @@ export default {
1132
1132
 
1133
1133
  Contributor rules emit autofixes the same way built-ins do, call `ctx.ReportFix(node, message, edits...)` or `ctx.ReportRangeFix(pos, end, message, edits...)`. The `rule/astutil` package re-exports the byte-range helpers built-ins use (`NodeText`, `KeywordStart`, `FindKeyword`, `TokenRange`). See the [contributor autofix path](https://ttsc.dev/docs/development/walkthroughs/lint#the-contributor-autofix-path) section for the full contract and an example.
1134
1134
 
1135
+ Contributor rules run on declaration files (`.d.ts`) by default. The engine skips its own value-level rules there — executable grammar cannot appear in a declaration file — but it cannot infer a third-party rule's shape, so contributors keep the conservative default. A rule that only inspects executable code can implement the optional `rule.DeclarationFileRule` marker (`VisitsDeclarationFiles() bool { return false }`) to get the same skip and save the dispatch on declaration-heavy projects.
1136
+
1135
1137
  ## Sponsors
1136
1138
 
1137
1139
  [![Sponsors](https://raw.githubusercontent.com/samchon/sponsor-images/refs/heads/master/public/circle.svg)](https://github.com/sponsors/samchon)
@@ -75,6 +75,18 @@ func (a contributorAdapter) NeedsTypeChecker() bool {
75
75
  return true
76
76
  }
77
77
 
78
+ // VisitsDeclarationFiles keeps contributor rules running on declaration
79
+ // files unless the contributor opts out through the public
80
+ // `rule.DeclarationFileRule` marker. Same conservative-default reasoning
81
+ // as NeedsTypeChecker: the host cannot infer a third-party rule's grammar
82
+ // shape, and a wrong skip silently loses findings.
83
+ func (a contributorAdapter) VisitsDeclarationFiles() bool {
84
+ if dr, ok := a.inner.(rule.DeclarationFileRule); ok {
85
+ return dr.VisitsDeclarationFiles()
86
+ }
87
+ return true
88
+ }
89
+
78
90
  // formatContributorAdapter is the FormatRule-tagged variant of
79
91
  // contributorAdapter. Wrapping the lint-only adapter (rather than
80
92
  // duplicating its method set) keeps the marker addition trivial and
@@ -0,0 +1,173 @@
1
+ // Declaration-file dispatch policy.
2
+ //
3
+ // The engine skips rules on `.d.ts` (and other declaration) sources unless
4
+ // the rule is known to produce legitimate findings there. Declaration files
5
+ // carry no executable code, so most value-level rules can never fire on one;
6
+ // walking them anyway is pure dispatch overhead on declaration-heavy
7
+ // projects (see issue #177).
8
+ //
9
+ // Three ways a rule participates in declaration files:
10
+ //
11
+ // - FormatRule markers: every format rule visits declaration files —
12
+ // `ttsc format` / `ttsc fix` must keep formatting hand-written `.d.ts`
13
+ // on the same boundary as other sources.
14
+ // - declarationFileRule: the optional interface a rule (built-in or
15
+ // contributor adapter) implements to answer for itself.
16
+ // - declarationFileRuleNames: the curated allowlist below for built-in
17
+ // rules, kept in one place so the whole audit stays reviewable.
18
+ //
19
+ // Audit principle for the allowlist: a rule is listed when its grammar
20
+ // shape occurs in declaration files under normal usage — type syntax,
21
+ // signatures, import/export forms, enums and their constant initializers,
22
+ // comments/JSDoc, identifier naming, or per-file metrics. Rules that need
23
+ // executable statements, expressions outside ambient-legal constant
24
+ // expressions, or runtime semantics (promises, DOM, JSX, test frameworks)
25
+ // are deliberately absent. When in doubt the rule is listed: a wrongly
26
+ // listed rule only costs dispatch time, while a wrongly skipped rule
27
+ // silently loses findings.
28
+ package linthost
29
+
30
+ // declarationFileRule marks rules that want to fire on declaration-file
31
+ // (`.d.ts`, `.d.mts`, `.d.cts`) inputs. Rules that neither implement this
32
+ // interface (returning true) nor appear in declarationFileRuleNames nor
33
+ // carry the FormatRule marker are skipped on every
34
+ // `file.IsDeclarationFile == true` source.
35
+ type declarationFileRule interface {
36
+ VisitsDeclarationFiles() bool
37
+ }
38
+
39
+ // ruleVisitsDeclarationFiles reports whether the engine dispatches `r` on
40
+ // declaration-file sources.
41
+ func ruleVisitsDeclarationFiles(r Rule) bool {
42
+ if isFormatRule(r) {
43
+ return true
44
+ }
45
+ if dr, ok := r.(declarationFileRule); ok {
46
+ return dr.VisitsDeclarationFiles()
47
+ }
48
+ return declarationFileRuleNames[r.Name()]
49
+ }
50
+
51
+ // declarationFileRuleNames is the audited allowlist of built-in rules whose
52
+ // grammar shapes occur in declaration files. Grouped by namespace; each
53
+ // group states the shape that justifies it. A name listed here must exist
54
+ // in the registry — the registry-parity test pins that.
55
+ var declarationFileRuleNames = map[string]bool{
56
+ // JSDoc lives on declarations; declaration files are its prime habitat.
57
+ "jsdoc/check-tag-names": true,
58
+ "jsdoc/check-values": true,
59
+ "jsdoc/empty-tags": true,
60
+ "jsdoc/no-types": true,
61
+ "jsdoc/reject-any-type": true,
62
+ "jsdoc/reject-function-type": true,
63
+ "jsdoc/require-description": true,
64
+ "jsdoc/require-param-description": true,
65
+ "jsdoc/require-param-name": true,
66
+ "jsdoc/require-property-description": true,
67
+ "jsdoc/require-property-name": true,
68
+ "jsdoc/require-returns-description": true,
69
+ "jsdoc/tsdoc-syntax": true,
70
+
71
+ // Import-graph boundaries: declaration files import and re-export, and
72
+ // an architectural boundary holds for hand-written `.d.ts` too.
73
+ "boundaries/dependencies": true,
74
+ "boundaries/element-types": true,
75
+ "boundaries/entry-point": true,
76
+ "boundaries/external": true,
77
+ "boundaries/no-private": true,
78
+ "boundaries/no-unknown": true,
79
+
80
+ // Core rules over shapes a declaration file contains: identifier
81
+ // naming, import/export forms, accessor and class-member signatures,
82
+ // per-file metrics, user-configured selectors, and the constant
83
+ // expressions enum initializers allow (bitwise flags, numeric
84
+ // literals).
85
+ "camelcase": true,
86
+ "grouped-accessor-pairs": true,
87
+ "id-length": true,
88
+ "max-classes-per-file": true,
89
+ "max-lines": true,
90
+ "max-params": true,
91
+ "no-bitwise": true,
92
+ "no-dupe-class-members": true,
93
+ "no-duplicate-imports": true,
94
+ "no-empty-named-blocks": true,
95
+ "no-irregular-whitespace": true,
96
+ "no-loss-of-precision": true,
97
+ "no-magic-numbers": true,
98
+ "no-mixed-operators": true,
99
+ "no-redeclare": true,
100
+ "no-restricted-imports": true,
101
+ "no-restricted-syntax": true,
102
+ "no-shadow": true,
103
+ "no-shadow-restricted-names": true,
104
+ "no-useless-computed-key": true,
105
+ "no-useless-rename": true,
106
+ "sort-imports": true,
107
+
108
+ // typescript/* rules over type syntax, signatures, enums, import/export
109
+ // type forms, comments, and declaration merging.
110
+ "typescript/adjacent-overload-signatures": true,
111
+ "typescript/array-type": true,
112
+ "typescript/ban-ts-comment": true,
113
+ "typescript/ban-tslint-comment": true,
114
+ "typescript/class-literal-property-style": true,
115
+ "typescript/consistent-indexed-object-style": true,
116
+ "typescript/consistent-type-definitions": true,
117
+ "typescript/consistent-type-exports": true,
118
+ "typescript/consistent-type-imports": true,
119
+ "typescript/explicit-function-return-type": true,
120
+ "typescript/explicit-member-accessibility": true,
121
+ "typescript/method-signature-style": true,
122
+ "typescript/no-deprecated": true,
123
+ "typescript/no-duplicate-enum-values": true,
124
+ "typescript/no-empty-interface": true,
125
+ "typescript/no-empty-object-type": true,
126
+ "typescript/no-explicit-any": true,
127
+ "typescript/no-extraneous-class": true,
128
+ "typescript/no-import-type-side-effects": true,
129
+ "typescript/no-invalid-void-type": true,
130
+ "typescript/no-magic-numbers": true,
131
+ "typescript/no-misused-new": true,
132
+ "typescript/no-mixed-enums": true,
133
+ "typescript/no-redundant-type-constituents": true,
134
+ "typescript/no-require-imports": true,
135
+ "typescript/no-restricted-types": true,
136
+ "typescript/no-unnecessary-qualifier": true,
137
+ "typescript/no-unnecessary-template-expression": true,
138
+ "typescript/no-unnecessary-type-arguments": true,
139
+ "typescript/no-unnecessary-type-constraint": true,
140
+ "typescript/no-unsafe-declaration-merging": true,
141
+ "typescript/no-unsafe-function-type": true,
142
+ "typescript/no-wrapper-object-types": true,
143
+ "typescript/prefer-enum-initializers": true,
144
+ "typescript/prefer-function-type": true,
145
+ "typescript/prefer-literal-enum-member": true,
146
+ "typescript/prefer-namespace-keyword": true,
147
+ "typescript/prefer-return-this-type": true,
148
+ "typescript/related-getter-setter-pairs": true,
149
+ "typescript/sort-type-constituents": true,
150
+ "typescript/triple-slash-reference": true,
151
+
152
+ // functional/* rules that police type shapes (not statements).
153
+ "functional/no-mixed-types": true,
154
+ "functional/no-return-void": true,
155
+ "functional/prefer-immutable-types": true,
156
+ "functional/prefer-property-signatures": true,
157
+ "functional/prefer-readonly-type": true,
158
+ "functional/readonly-type": true,
159
+ "functional/type-declaration-immutability": true,
160
+
161
+ // unicorn/* rules over file names, comments, identifier naming, and
162
+ // the numeric literals that appear in enum initializers and literal
163
+ // types.
164
+ "unicorn/empty-brace-spaces": true,
165
+ "unicorn/expiring-todo-comments": true,
166
+ "unicorn/filename-case": true,
167
+ "unicorn/no-abusive-eslint-disable": true,
168
+ "unicorn/no-empty-file": true,
169
+ "unicorn/no-keyword-prefix": true,
170
+ "unicorn/number-literal-case": true,
171
+ "unicorn/numeric-separators-style": true,
172
+ "unicorn/prevent-abbreviations": true,
173
+ }
@@ -534,6 +534,12 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
534
534
  // Context for every (node, rule) pair, which on a large program meant
535
535
  // millions of short-lived heap allocations and the GC pressure they
536
536
  // carry. Rules never mutate their Context, so reuse is safe.
537
+ //
538
+ // Declaration files only bind rules that opt into them (see
539
+ // declaration_rules.go): value-level rules can never fire on a `.d.ts`,
540
+ // so dispatching to them is pure overhead on declaration-heavy trees.
541
+ declarationFile := file.IsDeclarationFile
542
+ bound := 0
537
543
  byKind := make([][]boundRule, len(e.rules))
538
544
  ctxByRule := make(map[string]*Context, len(e.enabled))
539
545
  for kind, rules := range e.rules {
@@ -541,6 +547,9 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
541
547
  continue
542
548
  }
543
549
  for _, rule := range rules {
550
+ if declarationFile && !ruleVisitsDeclarationFiles(rule) {
551
+ continue
552
+ }
544
553
  name := rule.Name()
545
554
  ctx, built := ctxByRule[name]
546
555
  if !built {
@@ -563,6 +572,7 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
563
572
  continue
564
573
  }
565
574
  byKind[kind] = append(byKind[kind], boundRule{rule: rule, ctx: ctx})
575
+ bound++
566
576
  }
567
577
  }
568
578
 
@@ -572,23 +582,30 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
572
582
  // the heap on every invocation; converting to a method value with a
573
583
  // cached function field removes that allocation from the hot path —
574
584
  // ~38 % of pre-Opt-4 CPU was in the inner ForEachChild closure.
575
- w := &lintFileWalker{byKind: byKind, collect: collect}
576
- w.childCB = w.visitChild
577
-
578
- // SourceFile dispatches into its statement list directly; we walk
579
- // statements explicitly so the file node itself can be inspected by
580
- // rules (e.g., `ban-ts-comment` reads CommentDirectives off the
581
- // SourceFile).
582
- if k := int(shimast.KindSourceFile); k >= 0 && k < len(byKind) {
583
- for _, bound := range byKind[k] {
584
- runRuleCheck(bound.rule, bound.ctx, file.AsNode(), collect)
585
+ //
586
+ // With no bound rules at all (every active rule was filtered out, e.g.
587
+ // a declaration file where nothing opted in) the walk cannot produce a
588
+ // finding, so it is skipped entirely; inline directives are still
589
+ // parsed below so unknown-directive warnings stay file-complete.
590
+ if bound != 0 {
591
+ w := &lintFileWalker{byKind: byKind, collect: collect}
592
+ w.childCB = w.visitChild
593
+
594
+ // SourceFile dispatches into its statement list directly; we walk
595
+ // statements explicitly so the file node itself can be inspected by
596
+ // rules (e.g., `ban-ts-comment` reads CommentDirectives off the
597
+ // SourceFile).
598
+ if k := int(shimast.KindSourceFile); k >= 0 && k < len(byKind) {
599
+ for _, bound := range byKind[k] {
600
+ runRuleCheck(bound.rule, bound.ctx, file.AsNode(), collect)
601
+ }
585
602
  }
586
- }
587
603
 
588
- statements := file.Statements
589
- if statements != nil {
590
- for _, stmt := range statements.Nodes {
591
- w.walk(stmt)
604
+ statements := file.Statements
605
+ if statements != nil {
606
+ for _, stmt := range statements.Nodes {
607
+ w.walk(stmt)
608
+ }
592
609
  }
593
610
  }
594
611
  directives := parseLintInlineDirectives(file)
package/linthost/lsp.go CHANGED
@@ -502,8 +502,11 @@ func lspFormatBuffer(content string, opts *lspCommandOptions) (*lspWorkspaceEdit
502
502
 
503
503
  text := content
504
504
  converged := false
505
+ // The tsgo parser asserts on normalized (forward-slash) absolute paths;
506
+ // `target` comes from filepath.Abs and carries backslashes on Windows.
507
+ parseName := filepath.ToSlash(target)
505
508
  for pass := 0; pass < maxFormatPasses; pass++ {
506
- file := shimparser.ParseSourceFile(shimast.SourceFileParseOptions{FileName: target}, text, scriptKind)
509
+ file := shimparser.ParseSourceFile(shimast.SourceFileParseOptions{FileName: parseName}, text, scriptKind)
507
510
  if file == nil {
508
511
  // Match the disk path: a buffer we can't parse is a benign no-op, not a
509
512
  // hard error — don't fight the editor's own diagnostics on a dirty buffer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.15.2",
3
+ "version": "0.15.4",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -33,10 +33,10 @@
33
33
  "src"
34
34
  ],
35
35
  "devDependencies": {
36
- "@typescript/native-preview": "7.0.0-dev.20260421.2",
36
+ "@typescript/native-preview": "7.0.0-dev.20260615.1",
37
37
  "@types/node": "^25.3.0",
38
38
  "rimraf": "^6.1.2",
39
- "ttsc": "0.15.2"
39
+ "ttsc": "0.15.4"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
package/rule/rule.go CHANGED
@@ -97,6 +97,21 @@ type FormatRule interface {
97
97
  IsFormat() bool
98
98
  }
99
99
 
100
+ // DeclarationFileRule is an optional marker contributors implement to
101
+ // control whether their rule runs on declaration-file inputs (`.d.ts`,
102
+ // `.d.mts`, `.d.cts`). The engine skips most built-in rules on declaration
103
+ // files because value-level grammar cannot appear there; contributor rules
104
+ // keep the conservative default — they DO run on declaration files — since
105
+ // the host cannot infer a third-party rule's shape (mirror of the implicit
106
+ // checker default). A contributor whose rule inspects executable code only
107
+ // can implement this with `return false` to skip declaration files and
108
+ // save the dispatch on declaration-heavy projects; returning `true` is
109
+ // equivalent to not implementing the interface at all.
110
+ type DeclarationFileRule interface {
111
+ Rule
112
+ VisitsDeclarationFiles() bool
113
+ }
114
+
100
115
  // Reporter is the engine-supplied callback that records a finding. The
101
116
  // host implements this and passes it to `NewContext` when invoking a
102
117
  // contributor rule.