@ttsc/lint 0.19.0 → 0.19.2
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 +30 -0
- package/linthost/engine.go +1 -1
- package/linthost/host.go +3 -4
- package/linthost/project_engine.go +110 -30
- package/package.json +2 -2
- package/rule/project.go +73 -12
- package/rule/rule.go +3 -3
package/README.md
CHANGED
|
@@ -1182,6 +1182,36 @@ func init() { rule.RegisterProject(noCycles{}) }
|
|
|
1182
1182
|
|
|
1183
1183
|
Each project rule runs once per loaded Program, before file rules. `ctx.Identity` includes the invocation cwd, logical and physical config paths and roots, an optional explicit project root, the plugin-config origin, and a lifecycle id. `Report` marks the rule failed and emits one project finding; `Fail` marks it failed without a finding. Later file rules can call `ctx.ProjectResult(name)` and distinguish `absent`, `off`, `not_evaluated`, `passed`, and `failed`.
|
|
1184
1184
|
|
|
1185
|
+
Use `ctx.SetState(value)` when a later file rule needs the exact project binding selected during that check. The host returns the same value without interpreting or serializing it:
|
|
1186
|
+
|
|
1187
|
+
```go
|
|
1188
|
+
type projectBinding struct { /* contributor-owned fields */ }
|
|
1189
|
+
|
|
1190
|
+
func (projectGuard) Check(ctx *rule.ProjectContext) {
|
|
1191
|
+
ctx.SetState(loadProjectBinding(ctx.Identity))
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
func (guardedFileRule) Check(ctx *rule.Context, node *ast.Node) {
|
|
1195
|
+
result := ctx.ProjectResult("demo/project-guard")
|
|
1196
|
+
if result.Status != rule.ProjectRulePassed {
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
binding, ok := result.State.(*projectBinding)
|
|
1200
|
+
if !ok {
|
|
1201
|
+
return
|
|
1202
|
+
}
|
|
1203
|
+
if err := binding.Revalidate(); err != nil {
|
|
1204
|
+
result.Report(err.Error())
|
|
1205
|
+
return
|
|
1206
|
+
}
|
|
1207
|
+
useGuardedResource(binding, node)
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
`ProjectResult` is a snapshot of the current status and findings, while its `Report` and `Fail` methods remain live until file dispatch finishes. Call `ctx.ProjectResult(name)` again to observe a failure reported by an earlier helper. Equal messages are deduplicated, distinct messages are sorted, and finalized project findings stay ahead of file findings. `absent`, `off`, and `not_evaluated` results expose no state and their mutation methods do nothing.
|
|
1212
|
+
|
|
1213
|
+
State belongs to one loaded Program cycle. The host does not carry it into a watch or LSP rebuild: the new project check attaches that cycle's value and receives a new reporter. A reporter retained past file dispatch is inert. The host synchronizes status changes and finding deduplication across concurrent files. The contributor must create any required fresh state and synchronize mutable data inside its own value.
|
|
1214
|
+
|
|
1185
1215
|
Project rules use the normal `rules` map and `extends` order, but only global config entries may configure them. Any entry that contains `files`, including `files: []` or an `off` value, is rejected. Global `ignores` remain source-file filters rather than project-rule selectors. A later bare severity preserves the last explicit tuple options while replacing severity.
|
|
1186
1216
|
|
|
1187
1217
|
CLI, API, watch, and LSP runs carry the same project identity into the native host. Structured API findings use `file: null`. LSP publishes project findings once at the logical config URI with a zero range and no document version; project findings never provide fixes or code actions.
|
package/linthost/engine.go
CHANGED
|
@@ -694,7 +694,7 @@ func (e *Engine) Run(files []*shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
694
694
|
currentDirectory, _ = os.Getwd()
|
|
695
695
|
}
|
|
696
696
|
fileFindings := e.runFiles(files, checker, cycle.results, currentDirectory)
|
|
697
|
-
return append(cycle.
|
|
697
|
+
return append(cycle.finalize(), fileFindings...)
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
func (e *Engine) runFiles(
|
package/linthost/host.go
CHANGED
|
@@ -263,11 +263,10 @@ func (p *program) runLintCycle(engine *Engine) []*Finding {
|
|
|
263
263
|
}
|
|
264
264
|
files := p.userSourceFiles()
|
|
265
265
|
if p.projectCycle == nil {
|
|
266
|
-
|
|
267
|
-
p.projectCycle = &cycle
|
|
266
|
+
p.projectCycle = engine.evaluateProject(p.identity, files, p.checker)
|
|
268
267
|
}
|
|
269
|
-
|
|
270
|
-
return append(
|
|
268
|
+
fileFindings := engine.runFiles(files, p.checker, p.projectCycle.results, p.cwd)
|
|
269
|
+
return append(p.projectCycle.finalize(), fileFindings...)
|
|
271
270
|
}
|
|
272
271
|
|
|
273
272
|
// close drops the standalone lint checker. Safe to call on a nil receiver and
|
|
@@ -3,6 +3,7 @@ package linthost
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
5
|
"sort"
|
|
6
|
+
"sync"
|
|
6
7
|
|
|
7
8
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
8
9
|
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
@@ -10,7 +11,13 @@ import (
|
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
type projectCycleResults struct {
|
|
13
|
-
byName map[string]
|
|
14
|
+
byName map[string]projectCycleResult
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type projectCycleResult struct {
|
|
18
|
+
status publicrule.ProjectRuleStatus
|
|
19
|
+
severity Severity
|
|
20
|
+
reporter *projectReporter
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
func (r *projectCycleResults) ProjectResult(name string) publicrule.ProjectRuleResult {
|
|
@@ -21,22 +28,33 @@ func (r *projectCycleResults) ProjectResult(name string) publicrule.ProjectRuleR
|
|
|
21
28
|
if !ok {
|
|
22
29
|
return publicrule.ProjectRuleResult{Status: publicrule.ProjectRuleAbsent}
|
|
23
30
|
}
|
|
24
|
-
result.
|
|
25
|
-
|
|
31
|
+
if result.reporter == nil {
|
|
32
|
+
return publicrule.ProjectRuleResult{Status: result.status}
|
|
33
|
+
}
|
|
34
|
+
return result.reporter.snapshot()
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
type projectCycle struct {
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
finalizeOnce sync.Once
|
|
39
|
+
findings []*Finding
|
|
40
|
+
results *projectCycleResults
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
type projectReporter struct {
|
|
44
|
+
mu sync.Mutex
|
|
45
|
+
active bool
|
|
34
46
|
failed bool
|
|
35
47
|
messages map[string]struct{}
|
|
48
|
+
state any
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
func (r *projectReporter) Fail() {
|
|
39
|
-
if r
|
|
52
|
+
if r == nil {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
r.mu.Lock()
|
|
56
|
+
defer r.mu.Unlock()
|
|
57
|
+
if r.active {
|
|
40
58
|
r.failed = true
|
|
41
59
|
}
|
|
42
60
|
}
|
|
@@ -45,6 +63,11 @@ func (r *projectReporter) Report(message string) {
|
|
|
45
63
|
if r == nil {
|
|
46
64
|
return
|
|
47
65
|
}
|
|
66
|
+
r.mu.Lock()
|
|
67
|
+
defer r.mu.Unlock()
|
|
68
|
+
if !r.active {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
48
71
|
r.failed = true
|
|
49
72
|
if r.messages == nil {
|
|
50
73
|
r.messages = map[string]struct{}{}
|
|
@@ -52,13 +75,85 @@ func (r *projectReporter) Report(message string) {
|
|
|
52
75
|
r.messages[message] = struct{}{}
|
|
53
76
|
}
|
|
54
77
|
|
|
78
|
+
func (r *projectReporter) SetState(state any) {
|
|
79
|
+
if r == nil {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
r.mu.Lock()
|
|
83
|
+
defer r.mu.Unlock()
|
|
84
|
+
if r.active {
|
|
85
|
+
r.state = state
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func (r *projectReporter) snapshot() publicrule.ProjectRuleResult {
|
|
90
|
+
return r.snapshotLocked(false)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func (r *projectReporter) snapshotAndClose() publicrule.ProjectRuleResult {
|
|
94
|
+
return r.snapshotLocked(true)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func (r *projectReporter) snapshotLocked(close bool) publicrule.ProjectRuleResult {
|
|
98
|
+
if r == nil {
|
|
99
|
+
return publicrule.ProjectRuleResult{Status: publicrule.ProjectRuleAbsent}
|
|
100
|
+
}
|
|
101
|
+
r.mu.Lock()
|
|
102
|
+
defer r.mu.Unlock()
|
|
103
|
+
if close {
|
|
104
|
+
r.active = false
|
|
105
|
+
}
|
|
106
|
+
messages := make([]string, 0, len(r.messages))
|
|
107
|
+
for message := range r.messages {
|
|
108
|
+
messages = append(messages, message)
|
|
109
|
+
}
|
|
110
|
+
sort.Strings(messages)
|
|
111
|
+
status := publicrule.ProjectRulePassed
|
|
112
|
+
if r.failed {
|
|
113
|
+
status = publicrule.ProjectRuleFailed
|
|
114
|
+
}
|
|
115
|
+
findings := make([]publicrule.ProjectFinding, 0, len(messages))
|
|
116
|
+
for _, message := range messages {
|
|
117
|
+
findings = append(findings, publicrule.ProjectFinding{Message: message})
|
|
118
|
+
}
|
|
119
|
+
return publicrule.NewProjectRuleResult(status, r.state, findings, r)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (c *projectCycle) finalize() []*Finding {
|
|
123
|
+
if c == nil || c.results == nil {
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
c.finalizeOnce.Do(func() {
|
|
127
|
+
names := make([]string, 0, len(c.results.byName))
|
|
128
|
+
for name := range c.results.byName {
|
|
129
|
+
names = append(names, name)
|
|
130
|
+
}
|
|
131
|
+
sort.Strings(names)
|
|
132
|
+
for _, name := range names {
|
|
133
|
+
entry := c.results.byName[name]
|
|
134
|
+
if entry.reporter == nil {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
result := entry.reporter.snapshotAndClose()
|
|
138
|
+
for _, finding := range result.Findings {
|
|
139
|
+
c.findings = append(c.findings, &Finding{
|
|
140
|
+
Rule: name,
|
|
141
|
+
Severity: entry.severity,
|
|
142
|
+
Message: finding.Message,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
return append([]*Finding(nil), c.findings...)
|
|
148
|
+
}
|
|
149
|
+
|
|
55
150
|
func (e *Engine) evaluateProject(
|
|
56
151
|
identity publicrule.ProjectIdentity,
|
|
57
152
|
files []*shimast.SourceFile,
|
|
58
153
|
checker *shimchecker.Checker,
|
|
59
|
-
) projectCycle {
|
|
60
|
-
results := &projectCycleResults{byName: map[string]
|
|
61
|
-
cycle := projectCycle{results: results}
|
|
154
|
+
) *projectCycle {
|
|
155
|
+
results := &projectCycleResults{byName: map[string]projectCycleResult{}}
|
|
156
|
+
cycle := &projectCycle{results: results}
|
|
62
157
|
names := allProjectRuleNames()
|
|
63
158
|
if len(names) == 0 {
|
|
64
159
|
return cycle
|
|
@@ -68,11 +163,11 @@ func (e *Engine) evaluateProject(
|
|
|
68
163
|
for _, name := range names {
|
|
69
164
|
setting := e.projectSettings[name]
|
|
70
165
|
if !setting.Declared {
|
|
71
|
-
results.byName[name] =
|
|
166
|
+
results.byName[name] = projectCycleResult{status: publicrule.ProjectRuleNotEvaluated}
|
|
72
167
|
continue
|
|
73
168
|
}
|
|
74
169
|
if setting.Severity == SeverityOff {
|
|
75
|
-
results.byName[name] =
|
|
170
|
+
results.byName[name] = projectCycleResult{status: publicrule.ProjectRuleOff}
|
|
76
171
|
continue
|
|
77
172
|
}
|
|
78
173
|
adapter, exists := registeredProjectRules[name]
|
|
@@ -83,7 +178,7 @@ func (e *Engine) evaluateProject(
|
|
|
83
178
|
sources = e.projectSources(files)
|
|
84
179
|
sourcesResolved = true
|
|
85
180
|
}
|
|
86
|
-
reporter := &projectReporter{}
|
|
181
|
+
reporter := &projectReporter{active: true}
|
|
87
182
|
context := publicrule.NewProjectContext(
|
|
88
183
|
identity,
|
|
89
184
|
sources,
|
|
@@ -93,25 +188,10 @@ func (e *Engine) evaluateProject(
|
|
|
93
188
|
reporter,
|
|
94
189
|
)
|
|
95
190
|
runProjectRuleCheck(adapter, context, reporter)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
sort.Strings(messages)
|
|
101
|
-
status := publicrule.ProjectRulePassed
|
|
102
|
-
if reporter.failed {
|
|
103
|
-
status = publicrule.ProjectRuleFailed
|
|
104
|
-
}
|
|
105
|
-
result := publicrule.ProjectRuleResult{Status: status}
|
|
106
|
-
for _, message := range messages {
|
|
107
|
-
result.Findings = append(result.Findings, publicrule.ProjectFinding{Message: message})
|
|
108
|
-
cycle.findings = append(cycle.findings, &Finding{
|
|
109
|
-
Rule: name,
|
|
110
|
-
Severity: setting.Severity,
|
|
111
|
-
Message: message,
|
|
112
|
-
})
|
|
191
|
+
results.byName[name] = projectCycleResult{
|
|
192
|
+
severity: setting.Severity,
|
|
193
|
+
reporter: reporter,
|
|
113
194
|
}
|
|
114
|
-
results.byName[name] = result
|
|
115
195
|
}
|
|
116
196
|
return cycle
|
|
117
197
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"ttsc": "0.19.
|
|
40
|
+
"ttsc": "0.19.2"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
package/rule/project.go
CHANGED
|
@@ -39,16 +39,59 @@ type ProjectFinding struct {
|
|
|
39
39
|
Message string
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// ProjectRuleResult is
|
|
43
|
-
//
|
|
42
|
+
// ProjectRuleResult is one snapshot of a named project rule in the current
|
|
43
|
+
// Program cycle. State is the contributor-owned value attached during the
|
|
44
|
+
// project check; the host neither interprets nor synchronizes its contents.
|
|
45
|
+
// Findings is returned as a defensive copy by host result readers.
|
|
46
|
+
//
|
|
47
|
+
// Evaluated results retain a cycle-scoped failure channel through file-rule
|
|
48
|
+
// dispatch. Call Report or Fail immediately before a guarded operation, then
|
|
49
|
+
// call Context.ProjectResult again when the updated status is needed. Absent,
|
|
50
|
+
// off, and not-evaluated results have no state or live failure channel.
|
|
44
51
|
type ProjectRuleResult struct {
|
|
45
52
|
Status ProjectRuleStatus
|
|
53
|
+
State any
|
|
46
54
|
Findings []ProjectFinding
|
|
55
|
+
|
|
56
|
+
reporter ProjectReporter
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
59
|
+
// NewProjectRuleResult constructs one host-owned project-result snapshot.
|
|
60
|
+
// Contributor code normally receives this value from Context.ProjectResult
|
|
61
|
+
// and does not construct it.
|
|
62
|
+
func NewProjectRuleResult(
|
|
63
|
+
status ProjectRuleStatus,
|
|
64
|
+
state any,
|
|
65
|
+
findings []ProjectFinding,
|
|
66
|
+
reporter ProjectReporter,
|
|
67
|
+
) ProjectRuleResult {
|
|
68
|
+
return ProjectRuleResult{
|
|
69
|
+
Status: status,
|
|
70
|
+
State: state,
|
|
71
|
+
Findings: append([]ProjectFinding(nil), findings...),
|
|
72
|
+
reporter: reporter,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fail marks this evaluated project result failed without adding a finding.
|
|
77
|
+
// It is a no-op after file dispatch or for a result that was not evaluated.
|
|
78
|
+
func (r ProjectRuleResult) Fail() {
|
|
79
|
+
if r.reporter != nil {
|
|
80
|
+
r.reporter.Fail()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Report records one project finding and marks this evaluated result failed.
|
|
85
|
+
// Equal messages are deduplicated by the host. It is a no-op after file
|
|
86
|
+
// dispatch or for a result that was not evaluated.
|
|
87
|
+
func (r ProjectRuleResult) Report(message string) {
|
|
88
|
+
if r.reporter != nil {
|
|
89
|
+
r.reporter.Report(message)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ProjectResultReader supplies live project state to later file-rule contexts.
|
|
94
|
+
// Hosts return ProjectRuleAbsent for names with no registered project rule.
|
|
52
95
|
type ProjectResultReader interface {
|
|
53
96
|
ProjectResult(name string) ProjectRuleResult
|
|
54
97
|
}
|
|
@@ -77,7 +120,12 @@ type ProjectContext struct {
|
|
|
77
120
|
Severity Severity
|
|
78
121
|
Options json.RawMessage
|
|
79
122
|
|
|
80
|
-
reporter
|
|
123
|
+
reporter ProjectReporter
|
|
124
|
+
stateSetter projectStateSetter
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type projectStateSetter interface {
|
|
128
|
+
SetState(state any)
|
|
81
129
|
}
|
|
82
130
|
|
|
83
131
|
// NewProjectContext constructs the context a host passes to ProjectRule.Check.
|
|
@@ -91,13 +139,15 @@ func NewProjectContext(
|
|
|
91
139
|
reporter ProjectReporter,
|
|
92
140
|
) *ProjectContext {
|
|
93
141
|
copiedSources := append([]*shimast.SourceFile(nil), sources...)
|
|
142
|
+
stateSetter, _ := reporter.(projectStateSetter)
|
|
94
143
|
return &ProjectContext{
|
|
95
|
-
Identity:
|
|
96
|
-
Sources:
|
|
97
|
-
Checker:
|
|
98
|
-
Severity:
|
|
99
|
-
Options:
|
|
100
|
-
reporter:
|
|
144
|
+
Identity: identity,
|
|
145
|
+
Sources: copiedSources,
|
|
146
|
+
Checker: checker,
|
|
147
|
+
Severity: severity,
|
|
148
|
+
Options: append(json.RawMessage(nil), options...),
|
|
149
|
+
reporter: reporter,
|
|
150
|
+
stateSetter: stateSetter,
|
|
101
151
|
}
|
|
102
152
|
}
|
|
103
153
|
|
|
@@ -110,6 +160,17 @@ func (c *ProjectContext) DecodeOptions(out interface{}) error {
|
|
|
110
160
|
return json.Unmarshal(c.Options, out)
|
|
111
161
|
}
|
|
112
162
|
|
|
163
|
+
// SetState attaches one contributor-owned value to this rule's evaluated
|
|
164
|
+
// result. The exact value is returned to file rules in the same Program cycle;
|
|
165
|
+
// contributors own any synchronization needed inside it. The host does not
|
|
166
|
+
// serialize the value or retain it for a later watch or LSP rebuild.
|
|
167
|
+
func (c *ProjectContext) SetState(state any) {
|
|
168
|
+
if c == nil || c.stateSetter == nil || c.Severity == SeverityOff {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
c.stateSetter.SetState(state)
|
|
172
|
+
}
|
|
173
|
+
|
|
113
174
|
// Fail marks the current project rule failed without adding a diagnostic.
|
|
114
175
|
func (c *ProjectContext) Fail() {
|
|
115
176
|
if c == nil || c.reporter == nil || c.Severity == SeverityOff {
|
package/rule/rule.go
CHANGED
|
@@ -240,8 +240,8 @@ func NewContext(
|
|
|
240
240
|
return NewContextWithProjectResults(file, checker, severity, options, reporter, nil)
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
// NewContextWithProjectResults constructs a file-rule Context with the
|
|
244
|
-
//
|
|
243
|
+
// NewContextWithProjectResults constructs a file-rule Context with the live
|
|
244
|
+
// project results for the same loaded Program cycle.
|
|
245
245
|
func NewContextWithProjectResults(
|
|
246
246
|
file *shimast.SourceFile,
|
|
247
247
|
checker *shimchecker.Checker,
|
|
@@ -260,7 +260,7 @@ func NewContextWithProjectResults(
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
// ProjectResult returns
|
|
263
|
+
// ProjectResult returns a current snapshot for a named project rule in this
|
|
264
264
|
// file's Program cycle. Missing registrations return ProjectRuleAbsent.
|
|
265
265
|
func (c *Context) ProjectResult(name string) ProjectRuleResult {
|
|
266
266
|
if c == nil || c.results == nil {
|