@ttsc/lint 0.18.3 → 0.19.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.
Files changed (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +129 -25
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -0,0 +1,222 @@
1
+ // Grapheme-cluster string length, mirroring typescript-eslint's
2
+ // `getStringLength` utility. Directive descriptions are measured as Unicode
3
+ // extended grapheme clusters, the same units exposed by `Intl.Segmenter` with
4
+ // `granularity: "grapheme"`, rather than bytes, UTF-16 code units, or runes.
5
+ //
6
+ // The implementation follows the ordered extended-cluster rules in UAX #29.
7
+ // Its generated property tables are pinned to Unicode 16.0.0, the Unicode
8
+ // version used by the Node 24 runtime in this repository's CI. Regenerate the
9
+ // tables and official conformance corpus together when that runtime advances.
10
+ package linthost
11
+
12
+ //go:generate go run ../tools/graphemegen -root ..
13
+
14
+ // stringLength returns the number of Unicode extended grapheme clusters in s.
15
+ func stringLength(s string) int {
16
+ return graphemeCount(s)
17
+ }
18
+
19
+ // graphemeCount applies UAX #29's ordered boundary rules in one pass. The
20
+ // segmenter carries only the left contexts required by GB9c, GB11, and
21
+ // GB12/GB13, keeping long combining and regional-indicator runs linear.
22
+ func graphemeCount(s string) int {
23
+ count := 0
24
+ var segmenter graphemeSegmenter
25
+ for _, current := range s {
26
+ properties := graphemeProperties(current)
27
+ if segmenter.hasBoundaryBefore(properties) {
28
+ count++
29
+ }
30
+ segmenter.consume(properties)
31
+ }
32
+ return count
33
+ }
34
+
35
+ type graphemeRuneProperties struct {
36
+ breakClass graphemeBreakClass
37
+ indicConjunctClass indicConjunctBreakClass
38
+ extendedPictographic bool
39
+ }
40
+
41
+ type indicConjunctState uint8
42
+
43
+ const (
44
+ indicConjunctStateNone indicConjunctState = iota
45
+ indicConjunctStateConsonant
46
+ indicConjunctStateLinked
47
+ )
48
+
49
+ type emojiSequenceState uint8
50
+
51
+ const (
52
+ emojiSequenceStateNone emojiSequenceState = iota
53
+ emojiSequenceStatePictographic
54
+ emojiSequenceStateZWJ
55
+ )
56
+
57
+ type graphemeSegmenter struct {
58
+ hasPrevious bool
59
+ previous graphemeRuneProperties
60
+ regionalIndicators int
61
+ indicConjunct indicConjunctState
62
+ emojiSequence emojiSequenceState
63
+ }
64
+
65
+ // hasBoundaryBefore applies GB3 through GB999 in normative order. Start of
66
+ // text is always a boundary (GB1); end of text needs no explicit handling when
67
+ // only the number of clusters is required.
68
+ func (s *graphemeSegmenter) hasBoundaryBefore(current graphemeRuneProperties) bool {
69
+ if !s.hasPrevious {
70
+ return true
71
+ }
72
+ previous := s.previous.breakClass
73
+ next := current.breakClass
74
+
75
+ // GB3: keep CRLF together.
76
+ if previous == graphemeBreakCR && next == graphemeBreakLF {
77
+ return false
78
+ }
79
+ // GB4/GB5: otherwise break before and after controls.
80
+ if isGraphemeControlClass(previous) || isGraphemeControlClass(next) {
81
+ return true
82
+ }
83
+ // GB6-GB8: keep Hangul and other data-defined conjoining sequences.
84
+ if isConjoiningGraphemeJoin(previous, next) {
85
+ return false
86
+ }
87
+ // GB9/GB9a: extending characters, ZWJ, and spacing marks continue the
88
+ // current cluster.
89
+ if next == graphemeBreakExtend || next == graphemeBreakZWJ || next == graphemeBreakSpacingMark {
90
+ return false
91
+ }
92
+ // GB9b: Prepend joins the following non-control character.
93
+ if previous == graphemeBreakPrepend {
94
+ return false
95
+ }
96
+ // GB9c: an Indic consonant-linker sequence joins its next consonant.
97
+ if current.indicConjunctClass == indicConjunctBreakConsonant && s.indicConjunct == indicConjunctStateLinked {
98
+ return false
99
+ }
100
+ // GB11: Extended_Pictographic Extend* ZWJ joins the next pictograph.
101
+ if current.extendedPictographic && s.emojiSequence == emojiSequenceStateZWJ {
102
+ return false
103
+ }
104
+ // GB12/GB13: regional indicators join in pairs. The state is the
105
+ // uninterrupted RI count immediately before this boundary.
106
+ if next == graphemeBreakRegionalIndicator && s.regionalIndicators%2 == 1 {
107
+ return false
108
+ }
109
+ // GB999: all remaining positions are boundaries.
110
+ return true
111
+ }
112
+
113
+ func (s *graphemeSegmenter) consume(current graphemeRuneProperties) {
114
+ if current.breakClass == graphemeBreakRegionalIndicator {
115
+ s.regionalIndicators++
116
+ } else {
117
+ s.regionalIndicators = 0
118
+ }
119
+
120
+ switch current.indicConjunctClass {
121
+ case indicConjunctBreakConsonant:
122
+ s.indicConjunct = indicConjunctStateConsonant
123
+ case indicConjunctBreakExtend:
124
+ // Extend preserves a preceding consonant/linker context.
125
+ case indicConjunctBreakLinker:
126
+ if s.indicConjunct != indicConjunctStateNone {
127
+ s.indicConjunct = indicConjunctStateLinked
128
+ }
129
+ default:
130
+ s.indicConjunct = indicConjunctStateNone
131
+ }
132
+
133
+ switch {
134
+ case current.extendedPictographic:
135
+ s.emojiSequence = emojiSequenceStatePictographic
136
+ case current.breakClass == graphemeBreakExtend && s.emojiSequence == emojiSequenceStatePictographic:
137
+ // Extend preserves the pictograph context immediately before a ZWJ.
138
+ case current.breakClass == graphemeBreakZWJ && s.emojiSequence == emojiSequenceStatePictographic:
139
+ s.emojiSequence = emojiSequenceStateZWJ
140
+ default:
141
+ s.emojiSequence = emojiSequenceStateNone
142
+ }
143
+
144
+ s.previous = current
145
+ s.hasPrevious = true
146
+ }
147
+
148
+ func graphemeProperties(r rune) graphemeRuneProperties {
149
+ return graphemeRuneProperties{
150
+ breakClass: lookupGraphemeBreakClass(r),
151
+ indicConjunctClass: lookupIndicConjunctBreakClass(r),
152
+ extendedPictographic: isExtendedPictographic(r),
153
+ }
154
+ }
155
+
156
+ func lookupGraphemeBreakClass(r rune) graphemeBreakClass {
157
+ lo, hi := 0, len(graphemeBreakRanges)
158
+ for lo < hi {
159
+ middle := int(uint(lo+hi) >> 1)
160
+ candidate := graphemeBreakRanges[middle]
161
+ switch {
162
+ case r < candidate.lo:
163
+ hi = middle
164
+ case r > candidate.hi:
165
+ lo = middle + 1
166
+ default:
167
+ return candidate.class
168
+ }
169
+ }
170
+ return graphemeBreakOther
171
+ }
172
+
173
+ func lookupIndicConjunctBreakClass(r rune) indicConjunctBreakClass {
174
+ lo, hi := 0, len(indicConjunctBreakRanges)
175
+ for lo < hi {
176
+ middle := int(uint(lo+hi) >> 1)
177
+ candidate := indicConjunctBreakRanges[middle]
178
+ switch {
179
+ case r < candidate.lo:
180
+ hi = middle
181
+ case r > candidate.hi:
182
+ lo = middle + 1
183
+ default:
184
+ return candidate.class
185
+ }
186
+ }
187
+ return indicConjunctBreakNone
188
+ }
189
+
190
+ func isExtendedPictographic(r rune) bool {
191
+ lo, hi := 0, len(extendedPictographicRanges)
192
+ for lo < hi {
193
+ middle := int(uint(lo+hi) >> 1)
194
+ candidate := extendedPictographicRanges[middle]
195
+ switch {
196
+ case r < candidate.lo:
197
+ hi = middle
198
+ case r > candidate.hi:
199
+ lo = middle + 1
200
+ default:
201
+ return true
202
+ }
203
+ }
204
+ return false
205
+ }
206
+
207
+ func isGraphemeControlClass(class graphemeBreakClass) bool {
208
+ return class == graphemeBreakControl || class == graphemeBreakCR || class == graphemeBreakLF
209
+ }
210
+
211
+ func isConjoiningGraphemeJoin(previous, next graphemeBreakClass) bool {
212
+ switch previous {
213
+ case graphemeBreakL:
214
+ return next == graphemeBreakL || next == graphemeBreakV || next == graphemeBreakLV || next == graphemeBreakLVT
215
+ case graphemeBreakLV, graphemeBreakV:
216
+ return next == graphemeBreakV || next == graphemeBreakT
217
+ case graphemeBreakLVT, graphemeBreakT:
218
+ return next == graphemeBreakT
219
+ default:
220
+ return false
221
+ }
222
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.18.3",
4
- "description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
3
+ "version": "0.19.0",
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",
7
7
  "exports": {
@@ -26,6 +26,7 @@
26
26
  "files": [
27
27
  "README.md",
28
28
  "go.mod",
29
+ "internal",
29
30
  "lib",
30
31
  "linthost",
31
32
  "plugin",
@@ -36,7 +37,7 @@
36
37
  "@types/node": "^25.3.0",
37
38
  "rimraf": "^6.1.2",
38
39
  "typescript": "^7.0.2",
39
- "ttsc": "0.18.3"
40
+ "ttsc": "0.19.0"
40
41
  },
41
42
  "repository": {
42
43
  "type": "git",
@@ -0,0 +1,145 @@
1
+ package rule
2
+
3
+ import (
4
+ "encoding/json"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
8
+ )
9
+
10
+ // ProjectIdentity names one loaded TypeScript Program without conflating the
11
+ // caller's path spelling with the filesystem identity used by the compiler.
12
+ // Empty explicit fields mean the caller did not provide that channel.
13
+ type ProjectIdentity struct {
14
+ LifecycleID string `json:"lifecycleId"`
15
+ InvocationCwd string `json:"invocationCwd"`
16
+ LogicalConfigPath string `json:"logicalConfigPath"`
17
+ LogicalProjectRoot string `json:"logicalProjectRoot"`
18
+ PhysicalConfigPath string `json:"physicalConfigPath"`
19
+ PhysicalProjectRoot string `json:"physicalProjectRoot"`
20
+ ExplicitProjectRoot string `json:"explicitProjectRoot,omitempty"`
21
+ PluginConfigOrigin string `json:"pluginConfigOrigin,omitempty"`
22
+ }
23
+
24
+ // ProjectRuleStatus describes whether a named project rule exists, was
25
+ // configured, and completed during the current Program cycle.
26
+ type ProjectRuleStatus string
27
+
28
+ const (
29
+ ProjectRuleAbsent ProjectRuleStatus = "absent"
30
+ ProjectRuleOff ProjectRuleStatus = "off"
31
+ ProjectRuleNotEvaluated ProjectRuleStatus = "not_evaluated"
32
+ ProjectRulePassed ProjectRuleStatus = "passed"
33
+ ProjectRuleFailed ProjectRuleStatus = "failed"
34
+ )
35
+
36
+ // ProjectFinding is a non-file finding retained in a project rule's cycle
37
+ // result. Project findings never contain edits or source ranges.
38
+ type ProjectFinding struct {
39
+ Message string
40
+ }
41
+
42
+ // ProjectRuleResult is the finalized, read-only view of one named project
43
+ // rule. Findings is returned as a defensive copy by host result readers.
44
+ type ProjectRuleResult struct {
45
+ Status ProjectRuleStatus
46
+ Findings []ProjectFinding
47
+ }
48
+
49
+ // ProjectResultReader supplies finalized project state to later file-rule
50
+ // contexts. Hosts return ProjectRuleAbsent for names with no registered
51
+ // project rule.
52
+ type ProjectResultReader interface {
53
+ ProjectResult(name string) ProjectRuleResult
54
+ }
55
+
56
+ // ProjectRule is a contributor check that runs once for a loaded Program
57
+ // before any node rule dispatch. It has no AST visit list or synthetic file.
58
+ type ProjectRule interface {
59
+ Name() string
60
+ Check(ctx *ProjectContext)
61
+ }
62
+
63
+ // ProjectReporter is the cycle-scoped failure channel available to project
64
+ // helpers. Report records a deterministic project finding and also marks the
65
+ // current rule failed; Fail marks failure without adding a finding.
66
+ type ProjectReporter interface {
67
+ Fail()
68
+ Report(message string)
69
+ }
70
+
71
+ // ProjectContext contains the immutable inputs for one project-rule check.
72
+ // Sources is a defensive copy of the Program's tsconfig-selected user sources.
73
+ type ProjectContext struct {
74
+ Identity ProjectIdentity
75
+ Sources []*shimast.SourceFile
76
+ Checker *shimchecker.Checker
77
+ Severity Severity
78
+ Options json.RawMessage
79
+
80
+ reporter ProjectReporter
81
+ }
82
+
83
+ // NewProjectContext constructs the context a host passes to ProjectRule.Check.
84
+ // Contributor code normally receives this value and does not construct it.
85
+ func NewProjectContext(
86
+ identity ProjectIdentity,
87
+ sources []*shimast.SourceFile,
88
+ checker *shimchecker.Checker,
89
+ severity Severity,
90
+ options json.RawMessage,
91
+ reporter ProjectReporter,
92
+ ) *ProjectContext {
93
+ copiedSources := append([]*shimast.SourceFile(nil), sources...)
94
+ return &ProjectContext{
95
+ Identity: identity,
96
+ Sources: copiedSources,
97
+ Checker: checker,
98
+ Severity: severity,
99
+ Options: append(json.RawMessage(nil), options...),
100
+ reporter: reporter,
101
+ }
102
+ }
103
+
104
+ // DecodeOptions unmarshals the configured project-rule options into out. A
105
+ // missing options tuple leaves out unchanged and returns nil.
106
+ func (c *ProjectContext) DecodeOptions(out interface{}) error {
107
+ if c == nil || len(c.Options) == 0 {
108
+ return nil
109
+ }
110
+ return json.Unmarshal(c.Options, out)
111
+ }
112
+
113
+ // Fail marks the current project rule failed without adding a diagnostic.
114
+ func (c *ProjectContext) Fail() {
115
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
116
+ return
117
+ }
118
+ c.reporter.Fail()
119
+ }
120
+
121
+ // Report records one non-file project finding and marks the rule failed.
122
+ func (c *ProjectContext) Report(message string) {
123
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
124
+ return
125
+ }
126
+ c.reporter.Report(message)
127
+ }
128
+
129
+ var projectRegistry []ProjectRule
130
+
131
+ // RegisterProject adds a contributor project rule to the global registry.
132
+ // Hosts validate duplicate names after all contributor init functions finish.
133
+ func RegisterProject(r ProjectRule) {
134
+ if r == nil {
135
+ panic("rule: RegisterProject called with nil rule")
136
+ }
137
+ projectRegistry = append(projectRegistry, r)
138
+ }
139
+
140
+ // RegisteredProjects returns a defensive copy of all registered project rules.
141
+ func RegisteredProjects() []ProjectRule {
142
+ out := make([]ProjectRule, len(projectRegistry))
143
+ copy(out, projectRegistry)
144
+ return out
145
+ }
package/rule/rule.go CHANGED
@@ -112,6 +112,39 @@ type DeclarationFileRule interface {
112
112
  VisitsDeclarationFiles() bool
113
113
  }
114
114
 
115
+ // TypeAwareRule is an optional marker contributors implement to declare
116
+ // whether their rule reads `Context.Checker`. The host cannot infer a
117
+ // third-party rule's shape, so a contributor that does not implement this
118
+ // marker keeps the conservative default: it is treated as type-aware and
119
+ // receives a live checker.
120
+ //
121
+ // Being treated as type-aware is not free. The host creates a standalone
122
+ // checker spanning every source file, and the engine walks files serially so
123
+ // that checker is never accessed concurrently. A purely syntactic rule that
124
+ // never touches `Context.Checker` pays both costs for nothing.
125
+ //
126
+ // A contributor whose rule is AST-only can implement this with
127
+ // `NeedsTypeChecker() bool { return false }` to opt out of the checker path,
128
+ // preserving the engine's parallel file walk. Returning `true` is equivalent
129
+ // to not implementing the interface at all. A rule that returns `false` must
130
+ // not read `Context.Checker`: the host is free to leave it nil.
131
+ type TypeAwareRule interface {
132
+ Rule
133
+ NeedsTypeChecker() bool
134
+ }
135
+
136
+ // OptionsRule is an optional marker contributors implement to declare whether
137
+ // their rule accepts an options slot in its `[severity, options]` setting.
138
+ // Contributor rules default to accepting options for backward compatibility
139
+ // with the original public Context.Options contract. Return false for a
140
+ // genuinely optionless rule so the host can reject accidental payloads before
141
+ // linting. The domain-specific method name prevents an unrelated generic
142
+ // AcceptsOptions method on an existing contributor from opting in by accident.
143
+ // ProjectRule implementations may use the same marker.
144
+ type OptionsRule interface {
145
+ AcceptsTtscLintOptions() bool
146
+ }
147
+
115
148
  // Reporter is the engine-supplied callback that records a finding. The
116
149
  // host implements this and passes it to `NewContext` when invoking a
117
150
  // contributor rule.
@@ -191,6 +224,7 @@ type Context struct {
191
224
  Options json.RawMessage
192
225
 
193
226
  reporter Reporter
227
+ results ProjectResultReader
194
228
  }
195
229
 
196
230
  // NewContext constructs a Context for the engine to pass into a
@@ -202,14 +236,37 @@ func NewContext(
202
236
  severity Severity,
203
237
  options json.RawMessage,
204
238
  reporter Reporter,
239
+ ) *Context {
240
+ return NewContextWithProjectResults(file, checker, severity, options, reporter, nil)
241
+ }
242
+
243
+ // NewContextWithProjectResults constructs a file-rule Context with the
244
+ // finalized project state for the same loaded Program cycle.
245
+ func NewContextWithProjectResults(
246
+ file *shimast.SourceFile,
247
+ checker *shimchecker.Checker,
248
+ severity Severity,
249
+ options json.RawMessage,
250
+ reporter Reporter,
251
+ results ProjectResultReader,
205
252
  ) *Context {
206
253
  return &Context{
207
254
  File: file,
208
255
  Checker: checker,
209
256
  Severity: severity,
210
- Options: options,
257
+ Options: append(json.RawMessage(nil), options...),
211
258
  reporter: reporter,
259
+ results: results,
260
+ }
261
+ }
262
+
263
+ // ProjectResult returns the finalized state for a named project rule in this
264
+ // file's Program cycle. Missing registrations return ProjectRuleAbsent.
265
+ func (c *Context) ProjectResult(name string) ProjectRuleResult {
266
+ if c == nil || c.results == nil {
267
+ return ProjectRuleResult{Status: ProjectRuleAbsent}
212
268
  }
269
+ return c.results.ProjectResult(name)
213
270
  }
214
271
 
215
272
  // DecodeOptions unmarshals the rule's options blob into `out`. Returns
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ type TtscPluginDescriptor = {
22
22
  capabilities?: {
23
23
  diagnosticsTiming?: boolean;
24
24
  lsp?: boolean;
25
+ projectContextArgs?: boolean;
25
26
  threadingArgs?: boolean;
26
27
  };
27
28
  contributors?: TtscPluginContributor[];
@@ -43,6 +44,12 @@ type TtscPluginFactoryContext<TConfig> = {
43
44
  /** This descriptor's own path — the ESM-safe replacement for `__filename`. */
44
45
  filename: string;
45
46
  plugin: TConfig;
47
+ /**
48
+ * Caller-declared anchor for plugin config-file discovery, present when the
49
+ * embedder compiles through a generated tsconfig outside the project (see
50
+ * `ITtscPluginFactoryContext.pluginConfigDir`).
51
+ */
52
+ pluginConfigDir?: string;
46
53
  projectRoot: string;
47
54
  tsconfig: string;
48
55
  };
@@ -104,7 +111,9 @@ const FRAMEWORK_KEYS = new Set<string>([
104
111
  * (`lint.config.{ts,cts,mts,js,cjs,mjs,json}` or `ttsc-lint.config.*`). The
105
112
  * tsconfig plugin entry carries no rule or plugin surface — it optionally names
106
113
  * the config file via `configFile`, otherwise the file is discovered by walking
107
- * upward from the tsconfig directory.
114
+ * upward from the tsconfig directory (or from the caller-declared
115
+ * `pluginConfigDir` when the resolved tsconfig is a generated wrapper in a temp
116
+ * directory).
108
117
  *
109
118
  * The factory locates the config file, evaluates it (via ttsx for TS / ESM
110
119
  * sources, `require` for CommonJS, `JSON.parse` for JSON), reads every
@@ -121,7 +130,12 @@ export default function createTtscPlugin(
121
130
  // declared, so consumers (and the existing key-shape regression
122
131
  // tests) see the same surface as before this feature shipped.
123
132
  const descriptor: TtscPluginDescriptor = {
124
- capabilities: { diagnosticsTiming: true, lsp: true, threadingArgs: true },
133
+ capabilities: {
134
+ diagnosticsTiming: true,
135
+ lsp: true,
136
+ projectContextArgs: true,
137
+ threadingArgs: true,
138
+ },
125
139
  name: "@ttsc/lint",
126
140
  reportsTypeScriptDiagnostics: true,
127
141
  // `context.dirname` is this descriptor's own directory in every load mode —
@@ -187,9 +201,12 @@ type ConfigPluginEntry = { namespace: string; source: string };
187
201
  * Resolves the contributor lint plugins declared in the project's lint config
188
202
  * file.
189
203
  *
190
- * - When the tsconfig plugin entry sets `configFile`, that exact file is loaded.
204
+ * - When the tsconfig plugin entry sets `configFile`, that exact file is loaded
205
+ * (relative paths resolve against the tsconfig directory, or against the
206
+ * caller-declared `pluginConfigDir` when present).
191
207
  * - Otherwise a `lint.config.*` / `ttsc-lint.config.*` file is discovered by
192
- * walking upward from the tsconfig directory.
208
+ * walking upward from the tsconfig directory (or from `pluginConfigDir` when
209
+ * present).
193
210
  *
194
211
  * Returns an empty array when no config file is set or discovered — the Go
195
212
  * sidecar surfaces the missing-config error; the factory only needs to forward
@@ -201,7 +218,7 @@ function resolveConfigFileContributors(
201
218
  const configFile = readConfigFileOption(context);
202
219
  const configPath =
203
220
  configFile !== undefined
204
- ? path.resolve(tsconfigBaseDir(context), configFile)
221
+ ? path.resolve(pluginConfigBaseDir(context), configFile)
205
222
  : findLintConfigFile(context);
206
223
  if (!configPath || !fs.existsSync(configPath)) return [];
207
224
 
@@ -260,18 +277,15 @@ function readConfigFileOption(
260
277
  function findLintConfigFile(
261
278
  context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
262
279
  ): string | undefined {
263
- // Mirror the Go-side discovery origins: walk upward from the tsconfig
264
- // directory first, then fall back to the working directory. The fallback
265
- // covers callers that point at an out-of-tree tsconfig (e.g. a
266
- // TtscCompiler invocation whose wrapper tsconfig lives in the system temp
267
- // dir while cwd/projectRoot is the real project) without it the walk
268
- // dead-ends in the temp dir's ancestry and the project's config (and its
269
- // contributor plugins) are silently missed.
270
- const tsconfigDir = tsconfigBaseDir(context);
271
- const cwd = path.resolve(context.cwd ?? context.projectRoot);
272
- for (const origin of tsconfigDir === cwd
273
- ? [tsconfigDir]
274
- : [tsconfigDir, cwd]) {
280
+ // Mirror the Go side (driver.PluginConfigBaseDir): the caller-declared
281
+ // pluginConfigDir is the single walk origin when present it names the
282
+ // real project when the resolved tsconfig is a generated wrapper in a temp
283
+ // dir (@ttsc/unplugin's alias overlay), and it keeps the wrapper's temp
284
+ // ancestry out of the walk so a stray config planted there is never
285
+ // honored. Otherwise walk upward from the tsconfig directory first, then
286
+ // fall back to the working directory: that covers callers that point at an
287
+ // out-of-tree tsconfig without declaring an anchor.
288
+ for (const origin of discoveryConfigBaseDirs(context)) {
275
289
  const discovered = findLintConfigFileFrom(origin);
276
290
  if (discovered !== undefined) {
277
291
  return discovered;
@@ -280,6 +294,17 @@ function findLintConfigFile(
280
294
  return undefined;
281
295
  }
282
296
 
297
+ function discoveryConfigBaseDirs(
298
+ context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
299
+ ): string[] {
300
+ if (context.pluginConfigDir) {
301
+ return [path.resolve(context.cwd ?? ".", context.pluginConfigDir)];
302
+ }
303
+ const tsconfigDir = tsconfigBaseDir(context);
304
+ const cwd = path.resolve(context.cwd ?? context.projectRoot);
305
+ return tsconfigDir === cwd ? [tsconfigDir] : [tsconfigDir, cwd];
306
+ }
307
+
283
308
  function findLintConfigFileFrom(origin: string): string | undefined {
284
309
  // Mirror the Go-side discovery loop: walk from `origin` upward, returning
285
310
  // the first directory that has exactly one of the candidate filenames.
@@ -317,6 +342,22 @@ function findLintConfigFileFrom(origin: string): string | undefined {
317
342
  }
318
343
  }
319
344
 
345
+ /**
346
+ * Base directory for resolving a relative `configFile` from the tsconfig plugin
347
+ * entry. Mirrors the Go side (driver.PluginConfigBaseDir): the caller-declared
348
+ * pluginConfigDir wins when present — the resolved tsconfig is then a generated
349
+ * wrapper in a temp directory that no longer identifies the project — otherwise
350
+ * the tsconfig directory, falling back to the working directory.
351
+ */
352
+ function pluginConfigBaseDir(
353
+ context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
354
+ ): string {
355
+ if (context.pluginConfigDir) {
356
+ return path.resolve(context.cwd ?? ".", context.pluginConfigDir);
357
+ }
358
+ return tsconfigBaseDir(context);
359
+ }
360
+
320
361
  function tsconfigBaseDir(
321
362
  context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
322
363
  ): string {
@@ -38,6 +38,9 @@ export interface ITtscLintConfig {
38
38
  * Built-in rules are concrete interface properties for autocomplete and typo
39
39
  * checking. Namespaced families and contributor rules use the familiar slash
40
40
  * form such as `react/jsx-key` or `demo/no-demo`.
41
+ *
42
+ * Project-scoped contributor rules use this same map, but their configuration
43
+ * must come from entries without a `files` selector.
41
44
  */
42
45
  rules?: ITtscLintRules;
43
46
 
@@ -27,8 +27,9 @@ export type TtscLintRuleSetting =
27
27
  | readonly [TtscLintSeverity];
28
28
 
29
29
  /**
30
- * Per-rule severity-plus-options setting for rules that accept a typed options
31
- * object.
30
+ * Per-rule severity-plus-options setting for rules that accept one typed
31
+ * options object. Rules with canonical positional option lists expose a
32
+ * dedicated setting type instead.
32
33
  *
33
34
  * This is the tuple form ESLint users know — `[severity, options]` — kept
34
35
  * strongly typed by way of the rule's dedicated options interface (see
@@ -120,8 +120,9 @@ export interface ITtscLintFormat {
120
120
  endOfLine?: "lf" | "crlf";
121
121
 
122
122
  /**
123
- * Import sorting & merging. Off unless present; `true` enables it with
124
- * defaults, an object customizes.
123
+ * Import formatting. Off unless present; `true` sorts named specifiers and
124
+ * erased type-only imports with defaults, and an object customizes behavior.
125
+ * Runtime declaration reordering requires an explicit unsafe opt-in.
125
126
  *
126
127
  * @default false
127
128
  */