@ttsc/lint 0.20.1 → 0.22.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 +27 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +1283 -71
- package/lib/index.js.map +1 -1
- package/linthost/check_serve.go +305 -0
- package/linthost/config.go +2410 -98
- package/linthost/dispatch.go +7 -1
- package/linthost/host.go +6 -4
- package/linthost/lsp.go +55 -0
- package/linthost/project_inputs.go +241 -0
- package/linthost/serve.go +24 -3
- package/package.json +2 -2
- package/rule/project.go +58 -0
- package/src/index.ts +1363 -95
package/linthost/dispatch.go
CHANGED
|
@@ -76,7 +76,7 @@ func run(args []string) int {
|
|
|
76
76
|
// Don't pay contributor-registration cost for the version banner.
|
|
77
77
|
fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
|
|
78
78
|
return 0
|
|
79
|
-
case "check", "fix", "format", "build", "transform", "lsp-command-ids", "lsp-code-action-kinds", "lsp-diagnostics", "lsp-code-actions", "lsp-execute-command", "lsp-hints", "lsp-serve":
|
|
79
|
+
case "check", "check-serve", "fix", "format", "build", "transform", "project-inputs", "lsp-command-ids", "lsp-code-action-kinds", "lsp-diagnostics", "lsp-project-diagnostics", "lsp-code-actions", "lsp-execute-command", "lsp-hints", "lsp-serve":
|
|
80
80
|
default:
|
|
81
81
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
|
|
82
82
|
return 2
|
|
@@ -87,6 +87,8 @@ func run(args []string) int {
|
|
|
87
87
|
switch args[0] {
|
|
88
88
|
case "check":
|
|
89
89
|
return RunCheck(args[1:])
|
|
90
|
+
case "check-serve":
|
|
91
|
+
return RunCheckServe(os.Stdin, os.Stdout, args[1:])
|
|
90
92
|
case "fix":
|
|
91
93
|
return RunFix(args[1:])
|
|
92
94
|
case "format":
|
|
@@ -95,12 +97,16 @@ func run(args []string) int {
|
|
|
95
97
|
return RunBuild(args[1:])
|
|
96
98
|
case "transform":
|
|
97
99
|
return RunTransform(args[1:])
|
|
100
|
+
case "project-inputs":
|
|
101
|
+
return RunProjectInputs(args[1:])
|
|
98
102
|
case "lsp-command-ids":
|
|
99
103
|
return RunLSPCommandIDs(args[1:])
|
|
100
104
|
case "lsp-code-action-kinds":
|
|
101
105
|
return RunLSPCodeActionKinds(args[1:])
|
|
102
106
|
case "lsp-diagnostics":
|
|
103
107
|
return RunLSPDiagnostics(args[1:])
|
|
108
|
+
case "lsp-project-diagnostics":
|
|
109
|
+
return RunLSPProjectDiagnostics(args[1:])
|
|
104
110
|
case "lsp-code-actions":
|
|
105
111
|
return RunLSPCodeActions(args[1:])
|
|
106
112
|
case "lsp-execute-command":
|
package/linthost/host.go
CHANGED
|
@@ -322,10 +322,11 @@ func (p *program) sourceFileByPath(absPath string) *shimast.SourceFile {
|
|
|
322
322
|
// confirmed absPath is a known source file; a config edit or a new/removed file
|
|
323
323
|
// is handled by a full reload upstream, not here. tsgo returns a rebuilt Program
|
|
324
324
|
// when the edit reshaped the import graph, and that rebuilt Program is still
|
|
325
|
-
// correct,
|
|
326
|
-
|
|
325
|
+
// correct, but callers use the reused flag to distinguish incremental updates
|
|
326
|
+
// from full Program reconstruction in product telemetry.
|
|
327
|
+
func (p *program) applyChange(absPath string) bool {
|
|
327
328
|
if p == nil || p.tsProgram == nil {
|
|
328
|
-
return
|
|
329
|
+
return false
|
|
329
330
|
}
|
|
330
331
|
name := absPath
|
|
331
332
|
if file := p.sourceFileByPath(absPath); file != nil {
|
|
@@ -334,7 +335,7 @@ func (p *program) applyChange(absPath string) {
|
|
|
334
335
|
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
335
336
|
host := shimcompiler.NewCompilerHost(p.cwd, fs, bundled.LibPath(), nil, nil)
|
|
336
337
|
changed := shimtspath.ToPath(name, p.cwd, fs.UseCaseSensitiveFileNames())
|
|
337
|
-
newProg,
|
|
338
|
+
newProg, reused := p.tsProgram.UpdateProgram(changed, host, nil)
|
|
338
339
|
if newProg != nil {
|
|
339
340
|
p.tsProgram = newProg
|
|
340
341
|
if p.checker != nil {
|
|
@@ -344,6 +345,7 @@ func (p *program) applyChange(absPath string) {
|
|
|
344
345
|
// The prior cycle described the pre-edit Program; drop it so the next verb
|
|
345
346
|
// re-evaluates its rules over the updated ASTs.
|
|
346
347
|
p.projectCycle = nil
|
|
348
|
+
return reused
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
// userSourceFiles returns the tsconfig-selected source files the lint engine
|
package/linthost/lsp.go
CHANGED
|
@@ -189,6 +189,61 @@ func RunLSPDiagnostics(args []string) int {
|
|
|
189
189
|
return writeJSON(result)
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// RunLSPProjectDiagnostics prints the current project-rule publication without
|
|
193
|
+
// requiring an open TypeScript document.
|
|
194
|
+
func RunLSPProjectDiagnostics(args []string) int {
|
|
195
|
+
opts, ok := parseLSPCommandOptions("lsp-project-diagnostics", args)
|
|
196
|
+
if !ok {
|
|
197
|
+
return 2
|
|
198
|
+
}
|
|
199
|
+
result, code := computeLSPProjectDiagnostics(opts)
|
|
200
|
+
if code != 0 {
|
|
201
|
+
return code
|
|
202
|
+
}
|
|
203
|
+
return writeJSON(result)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// computeLSPProjectDiagnostics evaluates only project rules and returns an
|
|
207
|
+
// empty publication when they are disabled, allowing the proxy to clear the
|
|
208
|
+
// previous generation. A project that does not parse has no publication at
|
|
209
|
+
// all: acquireProgram returns no Program alongside its parse diagnostics, so
|
|
210
|
+
// the command prints null and the host keeps the producer's last good answer
|
|
211
|
+
// instead of clearing it with a broken evaluation.
|
|
212
|
+
func computeLSPProjectDiagnostics(opts *lspCommandOptions) (*lspProjectDiagnostics, int) {
|
|
213
|
+
rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
|
|
214
|
+
if err != nil {
|
|
215
|
+
fmt.Fprintln(os.Stderr, err)
|
|
216
|
+
return nil, 2
|
|
217
|
+
}
|
|
218
|
+
engine := NewEngineWithResolver(rules)
|
|
219
|
+
if err := engine.ConfigError(); err != nil {
|
|
220
|
+
fmt.Fprintln(os.Stderr, err)
|
|
221
|
+
return nil, 2
|
|
222
|
+
}
|
|
223
|
+
prog, _, closeProgram, err := acquireProgram(opts, engine.NeedsTypeChecker())
|
|
224
|
+
if closeProgram != nil {
|
|
225
|
+
defer closeProgram()
|
|
226
|
+
}
|
|
227
|
+
if err != nil {
|
|
228
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
229
|
+
return nil, 2
|
|
230
|
+
}
|
|
231
|
+
if prog == nil {
|
|
232
|
+
return nil, 0
|
|
233
|
+
}
|
|
234
|
+
publication := &lspProjectDiagnostics{
|
|
235
|
+
URI: fileURL(prog.identity.LogicalConfigPath),
|
|
236
|
+
Diagnostics: []lspDiagnostic{},
|
|
237
|
+
}
|
|
238
|
+
for _, finding := range prog.runProjectCycle(engine).finalize() {
|
|
239
|
+
publication.Diagnostics = append(
|
|
240
|
+
publication.Diagnostics,
|
|
241
|
+
findingToLSPDiagnostic(finding),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
return publication, 0
|
|
245
|
+
}
|
|
246
|
+
|
|
192
247
|
// computeLSPDiagnostics builds the diagnostics result for one file URI. Split
|
|
193
248
|
// from RunLSPDiagnostics so the resident lsp-serve loop can produce the same
|
|
194
249
|
// result against a warm Program without re-parsing per verb.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net/url"
|
|
7
|
+
"os"
|
|
8
|
+
slashpath "path"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"sort"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
publicrule "github.com/samchon/ttsc/packages/lint/rule"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// ProjectInputSnapshot is the normalized filesystem dependency publication
|
|
17
|
+
// shared by the CLI launcher and ttscserver.
|
|
18
|
+
type ProjectInputSnapshot struct {
|
|
19
|
+
Root string `json:"root"`
|
|
20
|
+
Files []string `json:"files"`
|
|
21
|
+
Globs []string `json:"globs"`
|
|
22
|
+
ReloadFiles []string `json:"reloadFiles,omitempty"`
|
|
23
|
+
ReloadDirectories []string `json:"reloadDirectories,omitempty"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// RunProjectInputs prints the enabled ProjectRule dependency snapshot without
|
|
27
|
+
// loading a TypeScript Program.
|
|
28
|
+
func RunProjectInputs(args []string) int {
|
|
29
|
+
opts, ok := parseLSPCommandOptions("project-inputs", args)
|
|
30
|
+
if !ok {
|
|
31
|
+
return 2
|
|
32
|
+
}
|
|
33
|
+
resolver, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
|
|
34
|
+
if err != nil {
|
|
35
|
+
fmt.Fprintln(os.Stderr, err)
|
|
36
|
+
return 2
|
|
37
|
+
}
|
|
38
|
+
identity := normalizeProjectIdentity(
|
|
39
|
+
opts.projectIdentity,
|
|
40
|
+
opts.cwd,
|
|
41
|
+
opts.tsconfig,
|
|
42
|
+
)
|
|
43
|
+
snapshot, err := collectProjectInputs(resolver, identity)
|
|
44
|
+
if err != nil {
|
|
45
|
+
fmt.Fprintln(os.Stderr, err)
|
|
46
|
+
return 2
|
|
47
|
+
}
|
|
48
|
+
return writeJSON(snapshot)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func collectProjectInputs(
|
|
52
|
+
resolver RuleResolver,
|
|
53
|
+
identity publicrule.ProjectIdentity,
|
|
54
|
+
) (ProjectInputSnapshot, error) {
|
|
55
|
+
engine := NewEngineWithResolver(resolver)
|
|
56
|
+
if err := engine.ConfigError(); err != nil {
|
|
57
|
+
return ProjectInputSnapshot{}, err
|
|
58
|
+
}
|
|
59
|
+
root := identity.PhysicalProjectRoot
|
|
60
|
+
if root == "" {
|
|
61
|
+
root = identity.LogicalProjectRoot
|
|
62
|
+
}
|
|
63
|
+
if root == "" {
|
|
64
|
+
root = identity.InvocationCwd
|
|
65
|
+
}
|
|
66
|
+
if root == "" {
|
|
67
|
+
return ProjectInputSnapshot{}, errors.New("@ttsc/lint: project inputs require a project root")
|
|
68
|
+
}
|
|
69
|
+
root = realProjectPath(root)
|
|
70
|
+
snapshot := ProjectInputSnapshot{Root: filepath.ToSlash(root)}
|
|
71
|
+
if source, ok := resolver.(interface{ ConfigPaths() []string }); ok {
|
|
72
|
+
for _, location := range source.ConfigPaths() {
|
|
73
|
+
normalized := filepath.ToSlash(realProjectPath(location))
|
|
74
|
+
// Keep configs in Files for older/LSP consumers that do not decode
|
|
75
|
+
// ReloadFiles yet, while CLI watch can classify the same path as an
|
|
76
|
+
// execution-selection transition.
|
|
77
|
+
snapshot.Files = append(snapshot.Files, normalized)
|
|
78
|
+
snapshot.ReloadFiles = append(snapshot.ReloadFiles, normalized)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if source, ok := resolver.(interface{ ConfigDirectories() []string }); ok {
|
|
82
|
+
for _, location := range source.ConfigDirectories() {
|
|
83
|
+
normalized := filepath.ToSlash(realProjectPath(location))
|
|
84
|
+
snapshot.ReloadDirectories = append(
|
|
85
|
+
snapshot.ReloadDirectories,
|
|
86
|
+
normalized,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var joined error
|
|
91
|
+
for _, name := range allProjectRuleNames() {
|
|
92
|
+
setting := engine.projectSettings[name]
|
|
93
|
+
if !setting.Declared || setting.Severity == SeverityOff {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
adapter := registeredProjectRules[name]
|
|
97
|
+
publisher, ok := adapter.inner.(publicrule.ProjectInputRule)
|
|
98
|
+
if !ok {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
inputs, err := callProjectInputs(
|
|
102
|
+
publisher,
|
|
103
|
+
publicrule.NewProjectInputContext(
|
|
104
|
+
identity,
|
|
105
|
+
publicrule.Severity(setting.Severity),
|
|
106
|
+
setting.Options,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
if err != nil {
|
|
110
|
+
joined = errors.Join(joined, fmt.Errorf("project rule %q inputs: %w", name, err))
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
for _, input := range inputs {
|
|
114
|
+
normalized, err := normalizeProjectInput(root, input)
|
|
115
|
+
if err != nil {
|
|
116
|
+
joined = errors.Join(joined, fmt.Errorf("project rule %q input: %w", name, err))
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
switch input.Kind {
|
|
120
|
+
case publicrule.ProjectInputFile:
|
|
121
|
+
snapshot.Files = append(snapshot.Files, normalized)
|
|
122
|
+
case publicrule.ProjectInputGlob:
|
|
123
|
+
snapshot.Globs = append(snapshot.Globs, normalized)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
snapshot.Files = uniqueProjectInputPatterns(snapshot.Files)
|
|
128
|
+
snapshot.Globs = uniqueProjectInputPatterns(snapshot.Globs)
|
|
129
|
+
snapshot.ReloadFiles = uniqueProjectInputPatterns(snapshot.ReloadFiles)
|
|
130
|
+
snapshot.ReloadDirectories = uniqueProjectInputPatterns(
|
|
131
|
+
snapshot.ReloadDirectories,
|
|
132
|
+
)
|
|
133
|
+
if joined != nil {
|
|
134
|
+
return ProjectInputSnapshot{}, joined
|
|
135
|
+
}
|
|
136
|
+
return snapshot, nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func callProjectInputs(
|
|
140
|
+
publisher publicrule.ProjectInputRule,
|
|
141
|
+
context *publicrule.ProjectInputContext,
|
|
142
|
+
) (inputs []publicrule.ProjectInput, err error) {
|
|
143
|
+
defer func() {
|
|
144
|
+
if recovered := recover(); recovered != nil {
|
|
145
|
+
err = fmt.Errorf("panicked while declaring inputs: %v", recovered)
|
|
146
|
+
}
|
|
147
|
+
}()
|
|
148
|
+
return append([]publicrule.ProjectInput(nil), publisher.ProjectInputs(context)...), nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func normalizeProjectInput(root string, input publicrule.ProjectInput) (string, error) {
|
|
152
|
+
if input.Kind != publicrule.ProjectInputFile && input.Kind != publicrule.ProjectInputGlob {
|
|
153
|
+
return "", fmt.Errorf("kind %q is not file or glob", input.Kind)
|
|
154
|
+
}
|
|
155
|
+
pattern := strings.TrimSpace(input.Pattern)
|
|
156
|
+
if pattern == "" {
|
|
157
|
+
return "", errors.New("pattern must not be empty")
|
|
158
|
+
}
|
|
159
|
+
if parsed, err := url.Parse(pattern); err == nil &&
|
|
160
|
+
(strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) {
|
|
161
|
+
return "", fmt.Errorf("remote URL %q is not a filesystem dependency", pattern)
|
|
162
|
+
}
|
|
163
|
+
pattern = filepath.FromSlash(pattern)
|
|
164
|
+
if !filepath.IsAbs(pattern) {
|
|
165
|
+
pattern = filepath.Join(root, pattern)
|
|
166
|
+
}
|
|
167
|
+
if input.Kind == publicrule.ProjectInputFile {
|
|
168
|
+
return filepath.ToSlash(realProjectPath(pattern)), nil
|
|
169
|
+
}
|
|
170
|
+
return filepath.ToSlash(realProjectGlob(pattern)), nil
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func realProjectGlob(pattern string) string {
|
|
174
|
+
clean := filepath.Clean(pattern)
|
|
175
|
+
volume := filepath.VolumeName(clean)
|
|
176
|
+
remainder := strings.TrimPrefix(clean, volume)
|
|
177
|
+
segments := strings.FieldsFunc(remainder, func(r rune) bool {
|
|
178
|
+
return r == '/' || r == '\\'
|
|
179
|
+
})
|
|
180
|
+
prefixCount := 0
|
|
181
|
+
for prefixCount < len(segments) && !strings.ContainsAny(segments[prefixCount], "*?") {
|
|
182
|
+
prefixCount++
|
|
183
|
+
}
|
|
184
|
+
prefix := volume + string(filepath.Separator)
|
|
185
|
+
if prefixCount > 0 {
|
|
186
|
+
prefix = filepath.Join(prefix, filepath.Join(segments[:prefixCount]...))
|
|
187
|
+
}
|
|
188
|
+
resolved := realProjectPath(prefix)
|
|
189
|
+
if prefixCount == len(segments) {
|
|
190
|
+
return resolved
|
|
191
|
+
}
|
|
192
|
+
return filepath.Join(resolved, filepath.Join(segments[prefixCount:]...))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func uniqueProjectInputPatterns(patterns []string) []string {
|
|
196
|
+
return uniqueProjectInputPatternsForFilesystem(
|
|
197
|
+
patterns,
|
|
198
|
+
isCaseInsensitiveFilesystem(),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func uniqueProjectInputPatternsForFilesystem(
|
|
203
|
+
patterns []string,
|
|
204
|
+
caseInsensitive bool,
|
|
205
|
+
) []string {
|
|
206
|
+
seen := map[string]string{}
|
|
207
|
+
for _, pattern := range patterns {
|
|
208
|
+
normalized := normalizeProjectInputPattern(pattern, caseInsensitive)
|
|
209
|
+
key := normalized
|
|
210
|
+
if caseInsensitive {
|
|
211
|
+
key = strings.ToLower(key)
|
|
212
|
+
}
|
|
213
|
+
seen[key] = normalized
|
|
214
|
+
}
|
|
215
|
+
out := make([]string, 0, len(seen))
|
|
216
|
+
for _, pattern := range seen {
|
|
217
|
+
out = append(out, pattern)
|
|
218
|
+
}
|
|
219
|
+
sort.Strings(out)
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func normalizeProjectInputPattern(
|
|
224
|
+
pattern string,
|
|
225
|
+
caseInsensitive bool,
|
|
226
|
+
) string {
|
|
227
|
+
if !caseInsensitive {
|
|
228
|
+
return filepath.ToSlash(filepath.Clean(filepath.FromSlash(pattern)))
|
|
229
|
+
}
|
|
230
|
+
slashed := strings.ReplaceAll(pattern, "\\", "/")
|
|
231
|
+
unc := strings.HasPrefix(slashed, "//")
|
|
232
|
+
normalized := slashpath.Clean(slashed)
|
|
233
|
+
if unc && !strings.HasPrefix(normalized, "//") {
|
|
234
|
+
normalized = "/" + normalized
|
|
235
|
+
}
|
|
236
|
+
return normalized
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func isCaseInsensitiveFilesystem() bool {
|
|
240
|
+
return filepath.Separator == '\\'
|
|
241
|
+
}
|
package/linthost/serve.go
CHANGED
|
@@ -126,7 +126,10 @@ func noopClose() {}
|
|
|
126
126
|
// or drops an entry for a full reload when a changed path is not one of its
|
|
127
127
|
// source files — a config edit, or a new or removed file, which tsgo's
|
|
128
128
|
// per-file UpdateProgram cannot express and a fresh load handles correctly.
|
|
129
|
-
func (c *residentProgramCache) applyChanges(
|
|
129
|
+
func (c *residentProgramCache) applyChanges(
|
|
130
|
+
paths []string,
|
|
131
|
+
external map[string]struct{},
|
|
132
|
+
) {
|
|
130
133
|
if len(paths) == 0 {
|
|
131
134
|
return
|
|
132
135
|
}
|
|
@@ -136,6 +139,9 @@ func (c *residentProgramCache) applyChanges(paths []string) {
|
|
|
136
139
|
fullReload := false
|
|
137
140
|
for _, path := range paths {
|
|
138
141
|
if prog.sourceFileByPath(path) == nil {
|
|
142
|
+
if _, ok := external[canonicalProjectPath("", realProjectPath(path))]; ok {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
139
145
|
fullReload = true
|
|
140
146
|
break
|
|
141
147
|
}
|
|
@@ -146,7 +152,9 @@ func (c *residentProgramCache) applyChanges(paths []string) {
|
|
|
146
152
|
continue
|
|
147
153
|
}
|
|
148
154
|
for _, path := range paths {
|
|
149
|
-
prog.
|
|
155
|
+
if prog.sourceFileByPath(path) != nil {
|
|
156
|
+
prog.applyChange(path)
|
|
157
|
+
}
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
}
|
|
@@ -193,6 +201,10 @@ type serveLSPRequest struct {
|
|
|
193
201
|
// Program does not already hold as a source (a config edit, a new or removed
|
|
194
202
|
// file) drops the entry for a full reload instead.
|
|
195
203
|
Changed []string `json:"changed,omitempty"`
|
|
204
|
+
// External marks changed URIs that belong to declared ProjectRule inputs.
|
|
205
|
+
// Unknown external files invalidate fresh rule state but not the TypeScript
|
|
206
|
+
// Program; a path that is also a source file still updates incrementally.
|
|
207
|
+
External []string `json:"external,omitempty"`
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
// serveLSPResponse is the reply to one request: the verb's JSON result verbatim
|
|
@@ -268,12 +280,18 @@ func handleServeLSPLine(line string, base *lspCommandOptions, encoder *json.Enco
|
|
|
268
280
|
}
|
|
269
281
|
if len(req.Changed) > 0 {
|
|
270
282
|
paths := make([]string, 0, len(req.Changed))
|
|
283
|
+
external := make(map[string]struct{}, len(req.External))
|
|
284
|
+
for _, uri := range req.External {
|
|
285
|
+
if path, err := filePathFromURI(uri); err == nil {
|
|
286
|
+
external[canonicalProjectPath("", realProjectPath(path))] = struct{}{}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
271
289
|
for _, uri := range req.Changed {
|
|
272
290
|
if path, err := filePathFromURI(uri); err == nil {
|
|
273
291
|
paths = append(paths, path)
|
|
274
292
|
}
|
|
275
293
|
}
|
|
276
|
-
residentPrograms.applyChanges(paths)
|
|
294
|
+
residentPrograms.applyChanges(paths, external)
|
|
277
295
|
}
|
|
278
296
|
opts := *base
|
|
279
297
|
opts.uri = req.URI
|
|
@@ -283,6 +301,9 @@ func handleServeLSPLine(line string, base *lspCommandOptions, encoder *json.Enco
|
|
|
283
301
|
case "lsp-diagnostics":
|
|
284
302
|
result, code := computeLSPDiagnostics(&opts)
|
|
285
303
|
encodeServeResult(encoder, result, code)
|
|
304
|
+
case "lsp-project-diagnostics":
|
|
305
|
+
result, code := computeLSPProjectDiagnostics(&opts)
|
|
306
|
+
encodeServeResult(encoder, result, code)
|
|
286
307
|
case "lsp-code-actions":
|
|
287
308
|
result, code := computeLSPCodeActions(&opts)
|
|
288
309
|
encodeServeResult(encoder, result, code)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
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.
|
|
40
|
+
"ttsc": "0.22.0"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
package/rule/project.go
CHANGED
|
@@ -103,6 +103,64 @@ type ProjectRule interface {
|
|
|
103
103
|
Check(ctx *ProjectContext)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ProjectInputKind distinguishes one exact local path from a glob population.
|
|
107
|
+
// Both kinds are resolved against ProjectIdentity.PhysicalProjectRoot by the
|
|
108
|
+
// host. Remote URLs are not project inputs.
|
|
109
|
+
type ProjectInputKind string
|
|
110
|
+
|
|
111
|
+
const (
|
|
112
|
+
ProjectInputFile ProjectInputKind = "file"
|
|
113
|
+
ProjectInputGlob ProjectInputKind = "glob"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// ProjectInput declares one local filesystem dependency of a ProjectRule.
|
|
117
|
+
// Pattern may be absolute or relative to the physical project root. Glob
|
|
118
|
+
// patterns support path-segment `*`, `?`, and `**`; exact files remain
|
|
119
|
+
// dependencies while missing.
|
|
120
|
+
type ProjectInput struct {
|
|
121
|
+
Kind ProjectInputKind `json:"kind"`
|
|
122
|
+
Pattern string `json:"pattern"`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ProjectInputRule is the optional dependency-publication contract for a
|
|
126
|
+
// ProjectRule. The host calls ProjectInputs after resolving the rule's options
|
|
127
|
+
// and physical project identity, without loading a TypeScript Program.
|
|
128
|
+
type ProjectInputRule interface {
|
|
129
|
+
ProjectInputs(ctx *ProjectInputContext) []ProjectInput
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ProjectInputContext contains the immutable configuration available while a
|
|
133
|
+
// ProjectRule declares its local filesystem dependencies.
|
|
134
|
+
type ProjectInputContext struct {
|
|
135
|
+
Identity ProjectIdentity
|
|
136
|
+
Severity Severity
|
|
137
|
+
Options json.RawMessage
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// NewProjectInputContext constructs the context passed to
|
|
141
|
+
// ProjectInputRule.ProjectInputs. Contributor code normally receives this value
|
|
142
|
+
// and does not construct it.
|
|
143
|
+
func NewProjectInputContext(
|
|
144
|
+
identity ProjectIdentity,
|
|
145
|
+
severity Severity,
|
|
146
|
+
options json.RawMessage,
|
|
147
|
+
) *ProjectInputContext {
|
|
148
|
+
return &ProjectInputContext{
|
|
149
|
+
Identity: identity,
|
|
150
|
+
Severity: severity,
|
|
151
|
+
Options: append(json.RawMessage(nil), options...),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// DecodeOptions unmarshals the configured project-rule options into out. A
|
|
156
|
+
// missing options tuple leaves out unchanged and returns nil.
|
|
157
|
+
func (c *ProjectInputContext) DecodeOptions(out interface{}) error {
|
|
158
|
+
if c == nil || len(c.Options) == 0 {
|
|
159
|
+
return nil
|
|
160
|
+
}
|
|
161
|
+
return json.Unmarshal(c.Options, out)
|
|
162
|
+
}
|
|
163
|
+
|
|
106
164
|
// ProjectReporter is the cycle-scoped failure channel available to project
|
|
107
165
|
// helpers. Report records a deterministic project finding and also marks the
|
|
108
166
|
// current rule failed; Fail marks failure without adding a finding.
|