@ttsc/lint 0.18.3 → 0.18.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.
@@ -34,12 +34,23 @@ import (
34
34
  // with a stderr warning. The host prefers a deterministic, debuggable
35
35
  // outcome over panicking inside startup.
36
36
  func registerContributors() {
37
- contributors := rule.Registered()
37
+ registered := rule.Registered()
38
+ contributors := make([]contributorMetadata, 0, len(registered))
39
+ for _, contributor := range registered {
40
+ metadata, err := inspectContributor(contributor)
41
+ if err != nil {
42
+ fmt.Fprintf(os.Stderr,
43
+ "@ttsc/lint: %v; dropping contributor entry\n",
44
+ err)
45
+ continue
46
+ }
47
+ contributors = append(contributors, metadata)
48
+ }
38
49
  sort.SliceStable(contributors, func(i, j int) bool {
39
- return contributors[i].Name() < contributors[j].Name()
50
+ return contributors[i].name < contributors[j].name
40
51
  })
41
52
  for _, contributor := range contributors {
42
- name := contributor.Name()
53
+ name := contributor.name
43
54
  if name == "" {
44
55
  fmt.Fprintln(os.Stderr, "@ttsc/lint: contributor rule with empty name ignored")
45
56
  continue
@@ -50,22 +61,78 @@ func registerContributors() {
50
61
  name)
51
62
  continue
52
63
  }
53
- if fr, ok := contributor.(rule.FormatRule); ok && fr.IsFormat() {
54
- Register(formatContributorAdapter{contributorAdapter{inner: contributor}})
64
+ adapter := newContributorAdapter(contributor)
65
+ if contributor.isFormat {
66
+ Register(formatContributorAdapter{adapter})
55
67
  continue
56
68
  }
57
- Register(contributorAdapter{inner: contributor})
69
+ Register(adapter)
70
+ }
71
+ }
72
+
73
+ // contributorMetadata is the immutable host-side view of a public contributor
74
+ // rule. Every contributor-defined metadata method is evaluated exactly once
75
+ // behind inspectContributor's recover barrier so a broken declaration cannot
76
+ // panic later during registry sorting or engine construction.
77
+ type contributorMetadata struct {
78
+ inner rule.Rule
79
+ isFormat bool
80
+ name string
81
+ visits []shimast.Kind
82
+ visitsDeclarationFiles bool
83
+ }
84
+
85
+ // inspectContributor evaluates the public rule metadata behind a recover
86
+ // barrier. Check itself is protected separately by runRuleCheck; this function
87
+ // covers the startup methods the engine must call before it can dispatch a
88
+ // node. A panicking contributor is rejected as one entry while the rest of the
89
+ // lint registry remains usable.
90
+ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, err error) {
91
+ defer func() {
92
+ if recovered := recover(); recovered != nil {
93
+ err = fmt.Errorf("contributor %T metadata panicked: %v", contributor, recovered)
94
+ }
95
+ }()
96
+ metadata = contributorMetadata{
97
+ inner: contributor,
98
+ name: contributor.Name(),
99
+ visits: append([]shimast.Kind(nil), contributor.Visits()...),
100
+ visitsDeclarationFiles: true,
101
+ }
102
+ if formatRule, ok := contributor.(rule.FormatRule); ok {
103
+ metadata.isFormat = formatRule.IsFormat()
104
+ }
105
+ if declarationRule, ok := contributor.(rule.DeclarationFileRule); ok {
106
+ metadata.visitsDeclarationFiles = declarationRule.VisitsDeclarationFiles()
107
+ }
108
+ return metadata, nil
109
+ }
110
+
111
+ // newContributorAdapter builds the engine-facing adapter from metadata already
112
+ // evaluated by inspectContributor, so no adapter method re-enters contributor
113
+ // code after startup. This is the only construction path; a zero-value
114
+ // contributorAdapter carries no usable metadata.
115
+ func newContributorAdapter(metadata contributorMetadata) contributorAdapter {
116
+ return contributorAdapter{
117
+ inner: metadata.inner,
118
+ name: metadata.name,
119
+ visits: metadata.visits,
120
+ visitsDeclarationFiles: metadata.visitsDeclarationFiles,
58
121
  }
59
122
  }
60
123
 
61
124
  // contributorAdapter wraps a public `rule.Rule` so the engine's
62
- // `Register` accepts it. Forward `Name` and `Visits` directly; bridge
63
- // `Check` by constructing a `rule.Context` whose `Reporter` calls back
64
- // into the engine's existing `Context.Report` / `ReportRange`. The
125
+ // `Register` accepts it. Name, Visits, and declaration-file policy are
126
+ // cached by inspectContributor; Check bridges through a `rule.Context`
127
+ // whose `Reporter` calls back into the engine's existing Context.Report /
128
+ // ReportRange. The
65
129
  // public `rule.Context` and the engine's internal `Context` share the
66
130
  // same shim AST types, so no wrapping / unwrapping of nodes is needed.
67
131
  type contributorAdapter struct {
68
- inner rule.Rule
132
+ inner rule.Rule
133
+ name string
134
+ visits []shimast.Kind
135
+ visitsDeclarationFiles bool
69
136
  }
70
137
 
71
138
  // NeedsTypeChecker keeps contributor rules on the historical checker path.
@@ -79,12 +146,10 @@ func (a contributorAdapter) NeedsTypeChecker() bool {
79
146
  // files unless the contributor opts out through the public
80
147
  // `rule.DeclarationFileRule` marker. Same conservative-default reasoning
81
148
  // as NeedsTypeChecker: the host cannot infer a third-party rule's grammar
82
- // shape, and a wrong skip silently loses findings.
149
+ // shape, and a wrong skip silently loses findings. The default-true /
150
+ // marker-override policy is applied once in inspectContributor.
83
151
  func (a contributorAdapter) VisitsDeclarationFiles() bool {
84
- if dr, ok := a.inner.(rule.DeclarationFileRule); ok {
85
- return dr.VisitsDeclarationFiles()
86
- }
87
- return true
152
+ return a.visitsDeclarationFiles
88
153
  }
89
154
 
90
155
  // formatContributorAdapter is the FormatRule-tagged variant of
@@ -98,8 +163,8 @@ type formatContributorAdapter struct {
98
163
 
99
164
  func (formatContributorAdapter) IsFormat() bool { return true }
100
165
 
101
- func (a contributorAdapter) Name() string { return a.inner.Name() }
102
- func (a contributorAdapter) Visits() []shimast.Kind { return a.inner.Visits() }
166
+ func (a contributorAdapter) Name() string { return a.name }
167
+ func (a contributorAdapter) Visits() []shimast.Kind { return a.visits }
103
168
  func (a contributorAdapter) Check(ctx *Context, node *shimast.Node) {
104
169
  if ctx == nil {
105
170
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.18.3",
3
+ "version": "0.18.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",
@@ -36,7 +36,7 @@
36
36
  "@types/node": "^25.3.0",
37
37
  "rimraf": "^6.1.2",
38
38
  "typescript": "^7.0.2",
39
- "ttsc": "0.18.3"
39
+ "ttsc": "0.18.4"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",