@ttsc/lint 0.28.5 → 0.29.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 CHANGED
@@ -1185,6 +1185,8 @@ func init() { rule.RegisterProject(noCycles{}) }
1185
1185
 
1186
1186
  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`.
1187
1187
 
1188
+ `ctx.ReportSeverity(rule.SeverityWarn, message)` overrides the level of one project finding. An off rule or off finding remains silent. A warning still marks the project result failed, while only an error fails the command. Snapshots retain each finding's severity, and duplicate messages keep the strongest reported level. Hosts implementing only `ProjectReporter` receive the message at the rule's configured level; `ProjectSeverityReporter` is the optional extension for individual levels.
1189
+
1188
1190
  When a project rule reads local files outside the TypeScript Program, implement `rule.ProjectInputRule` so watch and editor hosts can observe exactly those inputs:
1189
1191
 
1190
1192
  ```go
@@ -220,12 +220,13 @@ func (s *residentCheckState) run(
220
220
  opts.cwd,
221
221
  opts.tsconfig,
222
222
  loadProgramOptions{
223
- forceNoEmit: true,
224
- needsRuleChecker: engine.NeedsTypeChecker(),
225
- singleThreaded: opts.singleThreaded,
226
- checkers: opts.checkers,
227
- tsgoArgs: opts.tsgoArgs,
228
- projectIdentity: opts.projectIdentity,
223
+ forceNoEmit: true,
224
+ semanticConfigPath: opts.semanticConfigPath,
225
+ needsRuleChecker: engine.NeedsTypeChecker(),
226
+ singleThreaded: opts.singleThreaded,
227
+ checkers: opts.checkers,
228
+ tsgoArgs: opts.tsgoArgs,
229
+ projectIdentity: opts.projectIdentity,
229
230
  },
230
231
  )
231
232
  if err != nil {
@@ -74,6 +74,7 @@ func RunTransform(args []string) int {
74
74
 
75
75
  // RunTransformWithIO runs transform with invocation-owned output streams.
76
76
  func RunTransformWithIO(args []string, stdout, stderr io.Writer) int {
77
+ semanticConfigPath := os.Getenv(semanticConfigPathEnv)
77
78
  fs := flag.NewFlagSet("transform", flag.ContinueOnError)
78
79
  fs.SetOutput(stderr)
79
80
  file := fs.String("file", "", "absolute or cwd-relative path of the .ts file to transform")
@@ -122,12 +123,13 @@ func RunTransformWithIO(args []string, stdout, stderr io.Writer) int {
122
123
  engine.SetSerial(*singleThreaded)
123
124
 
124
125
  prog, parseDiags, err := loadProgram(resolvedCwd, *tsconfig, loadProgramOptions{
125
- forceEmit: true,
126
- needsRuleChecker: engine.NeedsTypeChecker(),
127
- singleThreaded: *singleThreaded,
128
- checkers: *checkers,
129
- tsgoArgs: tsgoArgs,
130
- projectIdentity: projectIdentity,
126
+ forceEmit: true,
127
+ semanticConfigPath: semanticConfigPath,
128
+ needsRuleChecker: engine.NeedsTypeChecker(),
129
+ singleThreaded: *singleThreaded,
130
+ checkers: *checkers,
131
+ tsgoArgs: tsgoArgs,
132
+ projectIdentity: projectIdentity,
131
133
  })
132
134
  if err != nil {
133
135
  fmt.Fprintf(stderr, "@ttsc/lint: %v\n", err)
@@ -201,21 +203,22 @@ func RunTransformWithIO(args []string, stdout, stderr io.Writer) int {
201
203
  }
202
204
 
203
205
  type subcommandOpts struct {
204
- cwd string
205
- tsconfig string
206
- pluginsJSON string
207
- emit bool
208
- noEmit bool
209
- quiet bool
210
- verbose bool
211
- diagnostics bool
212
- outDir string
213
- singleThreaded bool
214
- checkers int
215
- tsgoArgs []string
216
- projectIdentity publicrule.ProjectIdentity
217
- stdout io.Writer
218
- stderr io.Writer
206
+ cwd string
207
+ tsconfig string
208
+ pluginsJSON string
209
+ emit bool
210
+ noEmit bool
211
+ quiet bool
212
+ verbose bool
213
+ diagnostics bool
214
+ outDir string
215
+ semanticConfigPath string
216
+ singleThreaded bool
217
+ checkers int
218
+ tsgoArgs []string
219
+ projectIdentity publicrule.ProjectIdentity
220
+ stdout io.Writer
221
+ stderr io.Writer
219
222
  }
220
223
 
221
224
  // parseSubcommandFlags parses the shared flag set used by the `check`,
@@ -226,6 +229,7 @@ func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
226
229
  }
227
230
 
228
231
  func parseSubcommandFlagsWithIO(name string, args []string, stdout, stderr io.Writer) (*subcommandOpts, error) {
232
+ semanticConfigPath := os.Getenv(semanticConfigPathEnv)
229
233
  if stdout == nil {
230
234
  stdout = io.Discard
231
235
  }
@@ -267,21 +271,22 @@ func parseSubcommandFlagsWithIO(name string, args []string, stdout, stderr io.Wr
267
271
  return nil, err
268
272
  }
269
273
  return &subcommandOpts{
270
- cwd: resolvedCwd,
271
- tsconfig: *tsconfig,
272
- pluginsJSON: *pluginsJSON,
273
- emit: *emit,
274
- noEmit: *noEmit,
275
- quiet: *quiet,
276
- verbose: *verbose,
277
- diagnostics: *diagnostics || *extendedDiagnostics,
278
- outDir: *outDir,
279
- singleThreaded: *singleThreaded,
280
- checkers: *checkers,
281
- tsgoArgs: tsgoArgs,
282
- projectIdentity: projectIdentity,
283
- stdout: stdout,
284
- stderr: stderr,
274
+ cwd: resolvedCwd,
275
+ tsconfig: *tsconfig,
276
+ pluginsJSON: *pluginsJSON,
277
+ emit: *emit,
278
+ noEmit: *noEmit,
279
+ quiet: *quiet,
280
+ verbose: *verbose,
281
+ diagnostics: *diagnostics || *extendedDiagnostics,
282
+ outDir: *outDir,
283
+ semanticConfigPath: semanticConfigPath,
284
+ singleThreaded: *singleThreaded,
285
+ checkers: *checkers,
286
+ tsgoArgs: tsgoArgs,
287
+ projectIdentity: projectIdentity,
288
+ stdout: stdout,
289
+ stderr: stderr,
285
290
  }, nil
286
291
  }
287
292
 
@@ -332,14 +337,15 @@ func runProject(opts *subcommandOpts) int {
332
337
  engine.SetSerial(opts.singleThreaded)
333
338
 
334
339
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
335
- forceEmit: opts.emit,
336
- forceNoEmit: opts.noEmit,
337
- outDir: opts.outDir,
338
- needsRuleChecker: engine.NeedsTypeChecker(),
339
- singleThreaded: opts.singleThreaded,
340
- checkers: opts.checkers,
341
- tsgoArgs: opts.tsgoArgs,
342
- projectIdentity: opts.projectIdentity,
340
+ forceEmit: opts.emit,
341
+ forceNoEmit: opts.noEmit,
342
+ outDir: opts.outDir,
343
+ semanticConfigPath: opts.semanticConfigPath,
344
+ needsRuleChecker: engine.NeedsTypeChecker(),
345
+ singleThreaded: opts.singleThreaded,
346
+ checkers: opts.checkers,
347
+ tsgoArgs: opts.tsgoArgs,
348
+ projectIdentity: opts.projectIdentity,
343
349
  })
344
350
  if err != nil {
345
351
  fmt.Fprintf(opts.stderr, "@ttsc/lint: %v\n", err)
package/linthost/fix.go CHANGED
@@ -143,13 +143,14 @@ func runFix(opts *subcommandOpts) int {
143
143
  // NoEmit forced on. Returns (nil, 2) when loading or config parsing fails.
144
144
  func loadFixProgram(opts *subcommandOpts, needsRuleChecker bool) (*program, int) {
145
145
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
146
- forceNoEmit: true,
147
- outDir: opts.outDir,
148
- needsRuleChecker: needsRuleChecker,
149
- singleThreaded: opts.singleThreaded,
150
- checkers: opts.checkers,
151
- tsgoArgs: opts.tsgoArgs,
152
- projectIdentity: opts.projectIdentity,
146
+ forceNoEmit: true,
147
+ outDir: opts.outDir,
148
+ semanticConfigPath: opts.semanticConfigPath,
149
+ needsRuleChecker: needsRuleChecker,
150
+ singleThreaded: opts.singleThreaded,
151
+ checkers: opts.checkers,
152
+ tsgoArgs: opts.tsgoArgs,
153
+ projectIdentity: opts.projectIdentity,
153
154
  })
154
155
  if err != nil {
155
156
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
package/linthost/host.go CHANGED
@@ -33,6 +33,11 @@ import (
33
33
 
34
34
  var programLifecycleSequence atomic.Uint64
35
35
 
36
+ // semanticConfigPathEnv mirrors `driver.SemanticConfigPathEnv`: the internal
37
+ // channel that identifies the user-authored config behind a generated wrapper.
38
+ // This module cannot import the ttsc driver; see the package boundary above.
39
+ const semanticConfigPathEnv = "TTSC_SEMANTIC_CONFIG_PATH"
40
+
36
41
  // program bundles the tsgo Program with the parsed config and the standalone
37
42
  // checker used only by type-aware lint rules.
38
43
  type program struct {
@@ -54,9 +59,10 @@ type program struct {
54
59
  }
55
60
 
56
61
  type loadProgramOptions struct {
57
- forceEmit bool
58
- forceNoEmit bool
59
- outDir string
62
+ forceEmit bool
63
+ forceNoEmit bool
64
+ outDir string
65
+ semanticConfigPath string
60
66
  // needsRuleChecker asks loadProgram to create the standalone checker that
61
67
  // type-aware lint rules receive through Context.Checker.
62
68
  needsRuleChecker bool
@@ -118,6 +124,9 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
118
124
  if len(parsed.Errors) > 0 {
119
125
  return nil, parsed.Errors, nil
120
126
  }
127
+ if err := applySemanticConfigPath(parsed, options.semanticConfigPath); err != nil {
128
+ return nil, nil, err
129
+ }
121
130
  if options.forceNoEmit {
122
131
  forceNoEmit(parsed)
123
132
  }
@@ -158,6 +167,18 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
158
167
  }, nil, nil
159
168
  }
160
169
 
170
+ func applySemanticConfigPath(parsed *tsoptions.ParsedCommandLine, semanticConfigPath string) error {
171
+ configured := strings.TrimSpace(semanticConfigPath)
172
+ if configured == "" {
173
+ return nil
174
+ }
175
+ if !filepath.IsAbs(configured) {
176
+ return fmt.Errorf("linthost: semantic config path must be absolute: %s", configured)
177
+ }
178
+ parsed.ParsedConfig.CompilerOptions.ConfigFilePath = shimtspath.ResolvePath(configured)
179
+ return nil
180
+ }
181
+
161
182
  func normalizeProjectIdentity(
162
183
  identity publicrule.ProjectIdentity,
163
184
  cwd string,
package/linthost/lsp.go CHANGED
@@ -145,10 +145,11 @@ type lspCommandOptions struct {
145
145
  pluginsJSON string
146
146
  // rangeJSON carries the editor selection used to limit quickfix.ttsc
147
147
  // actions. Source fix-all and format actions remain document-wide.
148
- rangeJSON string
149
- tsconfig string
150
- uri string
151
- projectIdentity publicrule.ProjectIdentity
148
+ rangeJSON string
149
+ semanticConfigPath string
150
+ tsconfig string
151
+ uri string
152
+ projectIdentity publicrule.ProjectIdentity
152
153
  }
153
154
 
154
155
  // lspCommandIDs is the workspace/executeCommand ids owned by @ttsc/lint, shared
@@ -424,16 +425,17 @@ func parseLSPCommandOptions(name string, args []string) (*lspCommandOptions, boo
424
425
  return nil, false
425
426
  }
426
427
  return &lspCommandOptions{
427
- argumentsJSON: *argumentsJSON,
428
- command: *command,
429
- contextJSON: *contextJSON,
430
- contentStdin: *contentStdin,
431
- cwd: resolvedCwd,
432
- pluginsJSON: *pluginsJSON,
433
- rangeJSON: *rangeJSON,
434
- tsconfig: *tsconfig,
435
- uri: *uri,
436
- projectIdentity: projectIdentity,
428
+ argumentsJSON: *argumentsJSON,
429
+ command: *command,
430
+ contextJSON: *contextJSON,
431
+ contentStdin: *contentStdin,
432
+ cwd: resolvedCwd,
433
+ pluginsJSON: *pluginsJSON,
434
+ rangeJSON: *rangeJSON,
435
+ semanticConfigPath: os.Getenv(semanticConfigPathEnv),
436
+ tsconfig: *tsconfig,
437
+ uri: *uri,
438
+ projectIdentity: projectIdentity,
437
439
  }, true
438
440
  }
439
441
 
@@ -50,7 +50,8 @@ type projectReporter struct {
50
50
  mu sync.Mutex
51
51
  active bool
52
52
  failed bool
53
- messages map[string]struct{}
53
+ messages map[string]publicrule.Severity
54
+ severity publicrule.Severity
54
55
  state any
55
56
  }
56
57
 
@@ -66,19 +67,27 @@ func (r *projectReporter) Fail() {
66
67
  }
67
68
 
68
69
  func (r *projectReporter) Report(message string) {
70
+ if r != nil {
71
+ r.ReportSeverity(r.severity, message)
72
+ }
73
+ }
74
+
75
+ func (r *projectReporter) ReportSeverity(severity publicrule.Severity, message string) {
69
76
  if r == nil {
70
77
  return
71
78
  }
72
79
  r.mu.Lock()
73
80
  defer r.mu.Unlock()
74
- if !r.active {
81
+ if !r.active || severity == publicrule.SeverityOff {
75
82
  return
76
83
  }
77
84
  r.failed = true
78
85
  if r.messages == nil {
79
- r.messages = map[string]struct{}{}
86
+ r.messages = map[string]publicrule.Severity{}
87
+ }
88
+ if severity > r.messages[message] {
89
+ r.messages[message] = severity
80
90
  }
81
- r.messages[message] = struct{}{}
82
91
  }
83
92
 
84
93
  func (r *projectReporter) SetState(state any) {
@@ -120,7 +129,7 @@ func (r *projectReporter) snapshotLocked(close bool) publicrule.ProjectRuleResul
120
129
  }
121
130
  findings := make([]publicrule.ProjectFinding, 0, len(messages))
122
131
  for _, message := range messages {
123
- findings = append(findings, publicrule.ProjectFinding{Message: message})
132
+ findings = append(findings, publicrule.ProjectFinding{Message: message, Severity: r.messages[message]})
124
133
  }
125
134
  return publicrule.NewProjectRuleResult(status, r.state, findings, r)
126
135
  }
@@ -144,7 +153,7 @@ func (c *projectCycle) finalize() []*Finding {
144
153
  for _, finding := range result.Findings {
145
154
  c.findings = append(c.findings, &Finding{
146
155
  Rule: name,
147
- Severity: entry.severity,
156
+ Severity: Severity(finding.Severity),
148
157
  Message: finding.Message,
149
158
  })
150
159
  }
@@ -193,7 +202,7 @@ func (e *Engine) evaluateProject(
193
202
  if adapter.declinesTypeChecker {
194
203
  ruleChecker = nil
195
204
  }
196
- reporter := &projectReporter{active: true}
205
+ reporter := &projectReporter{active: true, severity: publicrule.Severity(setting.Severity)}
197
206
  context := publicrule.NewProjectContext(
198
207
  identity,
199
208
  sources,
package/linthost/serve.go CHANGED
@@ -80,9 +80,10 @@ func (c *residentProgramCache) acquire(
80
80
  }
81
81
  }
82
82
  prog, diags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
83
- forceNoEmit: true,
84
- needsRuleChecker: needsChecker,
85
- projectIdentity: opts.projectIdentity,
83
+ forceNoEmit: true,
84
+ needsRuleChecker: needsChecker,
85
+ projectIdentity: opts.projectIdentity,
86
+ semanticConfigPath: opts.semanticConfigPath,
86
87
  })
87
88
  if err != nil {
88
89
  return nil, nil, noopClose, err
@@ -172,9 +173,10 @@ func acquireProgram(
172
173
  return residentPrograms.acquire(opts, needsChecker)
173
174
  }
174
175
  prog, diags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
175
- forceNoEmit: true,
176
- needsRuleChecker: needsChecker,
177
- projectIdentity: opts.projectIdentity,
176
+ forceNoEmit: true,
177
+ needsRuleChecker: needsChecker,
178
+ projectIdentity: opts.projectIdentity,
179
+ semanticConfigPath: opts.semanticConfigPath,
178
180
  })
179
181
  if prog == nil {
180
182
  return prog, diags, noopClose, err
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.28.5",
3
+ "version": "0.29.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.28.5"
40
+ "ttsc": "0.29.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
package/rule/project.go CHANGED
@@ -36,7 +36,8 @@ const (
36
36
  // ProjectFinding is a non-file finding retained in a project rule's cycle
37
37
  // result. Project findings never contain edits or source ranges.
38
38
  type ProjectFinding struct {
39
- Message string
39
+ Message string
40
+ Severity Severity
40
41
  }
41
42
 
42
43
  // ProjectRuleResult is one snapshot of a named project rule in the current
@@ -169,6 +170,13 @@ type ProjectReporter interface {
169
170
  Report(message string)
170
171
  }
171
172
 
173
+ // ProjectSeverityReporter optionally accepts a severity for each finding.
174
+ // Reporting still marks the rule failed, including for warnings, so consumers
175
+ // cannot treat an incomplete project result as a clean one.
176
+ type ProjectSeverityReporter interface {
177
+ ReportSeverity(severity Severity, message string)
178
+ }
179
+
172
180
  // ProjectContext contains the immutable inputs for one project-rule check.
173
181
  //
174
182
  // Sources is a defensive copy of the user sources the host read for this cycle:
@@ -256,6 +264,19 @@ func (c *ProjectContext) Report(message string) {
256
264
  c.reporter.Report(message)
257
265
  }
258
266
 
267
+ // ReportSeverity records a finding at an explicit level. An off rule or off
268
+ // finding remains silent. Hosts without this extension use the rule's level.
269
+ func (c *ProjectContext) ReportSeverity(severity Severity, message string) {
270
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff || severity == SeverityOff {
271
+ return
272
+ }
273
+ if reporter, ok := c.reporter.(ProjectSeverityReporter); ok {
274
+ reporter.ReportSeverity(severity, message)
275
+ } else {
276
+ c.reporter.Report(message)
277
+ }
278
+ }
279
+
259
280
  var projectRegistry []ProjectRule
260
281
 
261
282
  // RegisterProject adds a contributor project rule to the global registry.