@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 +2 -0
- package/linthost/contrib_adapter.go +12 -0
- package/linthost/declaration_rules.go +173 -0
- package/linthost/engine.go +32 -15
- package/linthost/lsp.go +4 -1
- package/package.json +3 -3
- package/rule/rule.go +15 -0
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
|
[](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
|
+
}
|
package/linthost/engine.go
CHANGED
|
@@ -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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|