@ttsc/lint 0.18.4 → 0.19.1

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 +125 -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 +47 -8
  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 +159 -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 +231 -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 +206 -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,154 @@
1
+ // Shared parser-aware comment enumeration for every rule or pass that needs
2
+ // exact comment byte ranges (inline directives, ban-ts-comment, and switch
3
+ // default markers).
4
+ package linthost
5
+
6
+ import (
7
+ "sort"
8
+
9
+ shimast "github.com/microsoft/typescript-go/shim/ast"
10
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
11
+ )
12
+
13
+ type commentToken struct {
14
+ kind shimast.Kind
15
+ pos int
16
+ end int
17
+ }
18
+
19
+ type sourceSpan struct {
20
+ pos int
21
+ end int
22
+ }
23
+
24
+ // forEachCommentToken visits every real comment in `file` in source order.
25
+ //
26
+ // TypeScript's parser, rather than a context-free scanner, owns the lexical
27
+ // goal for regular expressions, templates, and JSX. The parsed AST retains
28
+ // those context-sensitive tokens as exact spans. We treat those spans as
29
+ // opaque and scan only the gaps between them, where slash-shaped bytes are
30
+ // ordinary JavaScript trivia or punctuation. This preserves the scanner's
31
+ // complete comment and line-terminator behavior without guessing whether a
32
+ // slash is division, manually tracking template braces, or special-casing JSX
33
+ // strings. Exact ranges are deduplicated before the callback runs because a
34
+ // recovery AST can expose overlapping token nodes for malformed source.
35
+ func forEachCommentToken(file *shimast.SourceFile, visit func(kind shimast.Kind, pos, end int)) {
36
+ if file == nil || visit == nil {
37
+ return
38
+ }
39
+ text := file.Text()
40
+ opaque := parserOpaqueTokenSpans(file)
41
+ comments := make([]commentToken, 0)
42
+ seen := make(map[sourceSpan]struct{})
43
+ gapScanner := shimscanner.NewScanner()
44
+ collect := func(kind shimast.Kind, pos, end int) {
45
+ if pos < 0 || end <= pos || end > len(text) {
46
+ return
47
+ }
48
+ span := sourceSpan{pos: pos, end: end}
49
+ if _, ok := seen[span]; ok {
50
+ return
51
+ }
52
+ seen[span] = struct{}{}
53
+ comments = append(comments, commentToken{kind: kind, pos: pos, end: end})
54
+ }
55
+
56
+ cursor := 0
57
+ for _, span := range opaque {
58
+ if cursor < span.pos {
59
+ scanCommentGap(gapScanner, text, cursor, span.pos, collect)
60
+ }
61
+ if span.end > cursor {
62
+ cursor = span.end
63
+ }
64
+ }
65
+ if cursor < len(text) {
66
+ scanCommentGap(gapScanner, text, cursor, len(text), collect)
67
+ }
68
+
69
+ sort.Slice(comments, func(i, j int) bool {
70
+ if comments[i].pos != comments[j].pos {
71
+ return comments[i].pos < comments[j].pos
72
+ }
73
+ return comments[i].end < comments[j].end
74
+ })
75
+ for _, comment := range comments {
76
+ visit(comment.kind, comment.pos, comment.end)
77
+ }
78
+ }
79
+
80
+ // parserOpaqueTokenSpans returns merged source ranges for the parser-owned
81
+ // tokens whose contents a context-free scanner cannot classify: strings,
82
+ // regex literals, template heads/middles/tails, and JSX text. Numeric literals,
83
+ // identifiers, keywords, and punctuation remain in the scanned gaps because
84
+ // they cannot contain comment-shaped source bytes; keeping them there avoids a
85
+ // scanner reset for every ordinary token.
86
+ func parserOpaqueTokenSpans(file *shimast.SourceFile) []sourceSpan {
87
+ spans := make([]sourceSpan, 0)
88
+ var walk func(*shimast.Node)
89
+ walk = func(node *shimast.Node) {
90
+ if node == nil {
91
+ return
92
+ }
93
+ if parserOpaqueTokenKind(node.Kind) {
94
+ pos := shimscanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/)
95
+ if pos >= 0 && node.End() > pos && node.End() <= len(file.Text()) {
96
+ spans = append(spans, sourceSpan{pos: pos, end: node.End()})
97
+ }
98
+ return
99
+ }
100
+ node.ForEachChild(func(child *shimast.Node) bool {
101
+ walk(child)
102
+ return false
103
+ })
104
+ }
105
+ walk(file.AsNode())
106
+ sort.Slice(spans, func(i, j int) bool {
107
+ if spans[i].pos != spans[j].pos {
108
+ return spans[i].pos < spans[j].pos
109
+ }
110
+ return spans[i].end < spans[j].end
111
+ })
112
+ if len(spans) < 2 {
113
+ return spans
114
+ }
115
+ merged := spans[:1]
116
+ for _, span := range spans[1:] {
117
+ last := &merged[len(merged)-1]
118
+ if span.pos <= last.end {
119
+ if span.end > last.end {
120
+ last.end = span.end
121
+ }
122
+ continue
123
+ }
124
+ merged = append(merged, span)
125
+ }
126
+ return merged
127
+ }
128
+
129
+ func parserOpaqueTokenKind(kind shimast.Kind) bool {
130
+ return kind >= shimast.KindStringLiteral && kind <= shimast.KindLastLiteralToken ||
131
+ kind >= shimast.KindFirstTemplateToken && kind <= shimast.KindLastTemplateToken
132
+ }
133
+
134
+ // scanCommentGap scans one parser-classified non-token gap. Context-sensitive
135
+ // token bodies never enter this function, so every comment trivia token it
136
+ // returns is a real source comment. Scanning the isolated gap also preserves
137
+ // CRLF and Unicode line-terminator behavior without range arithmetic in a
138
+ // second comment parser.
139
+ func scanCommentGap(scanner *shimscanner.Scanner, text string, from, to int, visit func(kind shimast.Kind, pos, end int)) {
140
+ if scanner == nil || from < 0 || to <= from || to > len(text) {
141
+ return
142
+ }
143
+ scanner.SetText(text[from:to])
144
+ scanner.SetSkipTrivia(false)
145
+ for {
146
+ kind := scanner.Scan()
147
+ switch kind {
148
+ case shimast.KindEndOfFile:
149
+ return
150
+ case shimast.KindSingleLineCommentTrivia, shimast.KindMultiLineCommentTrivia:
151
+ visit(kind, from+scanner.TokenStart(), from+scanner.TokenEnd())
152
+ }
153
+ }
154
+ }
@@ -27,13 +27,20 @@ import (
27
27
  shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
28
28
  shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
29
29
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
30
+
31
+ publicrule "github.com/samchon/ttsc/packages/lint/rule"
30
32
  )
31
33
 
32
34
  // RunCheck implements `@ttsc/lint check` — typecheck + lint, no emit.
33
35
  func RunCheck(args []string) int {
34
- opts, err := parseSubcommandFlags("check", args)
36
+ return RunCheckWithIO(args, os.Stdout, os.Stderr)
37
+ }
38
+
39
+ // RunCheckWithIO runs check with invocation-owned output streams.
40
+ func RunCheckWithIO(args []string, stdout, stderr io.Writer) int {
41
+ opts, err := parseSubcommandFlagsWithIO("check", args, stdout, stderr)
35
42
  if err != nil {
36
- fmt.Fprintln(os.Stderr, err)
43
+ fmt.Fprintln(stderr, err)
37
44
  return 2
38
45
  }
39
46
  opts.noEmit = true
@@ -43,9 +50,14 @@ func RunCheck(args []string) int {
43
50
  // RunBuild implements `@ttsc/lint build` — same diagnostic flow as
44
51
  // `check`, plus the tsgo emit pipeline when emit is requested.
45
52
  func RunBuild(args []string) int {
46
- opts, err := parseSubcommandFlags("build", args)
53
+ return RunBuildWithIO(args, os.Stdout, os.Stderr)
54
+ }
55
+
56
+ // RunBuildWithIO runs build with invocation-owned output streams.
57
+ func RunBuildWithIO(args []string, stdout, stderr io.Writer) int {
58
+ opts, err := parseSubcommandFlagsWithIO("build", args, stdout, stderr)
47
59
  if err != nil {
48
- fmt.Fprintln(os.Stderr, err)
60
+ fmt.Fprintln(stderr, err)
49
61
  return 2
50
62
  }
51
63
  return runProject(opts)
@@ -55,13 +67,19 @@ func RunBuild(args []string) int {
55
67
  // still run for the whole program (lint quality depends on context), but
56
68
  // emit is restricted to the requested file's JS output.
57
69
  func RunTransform(args []string) int {
70
+ return RunTransformWithIO(args, os.Stdout, os.Stderr)
71
+ }
72
+
73
+ // RunTransformWithIO runs transform with invocation-owned output streams.
74
+ func RunTransformWithIO(args []string, stdout, stderr io.Writer) int {
58
75
  fs := flag.NewFlagSet("transform", flag.ContinueOnError)
59
- fs.SetOutput(os.Stderr)
76
+ fs.SetOutput(stderr)
60
77
  file := fs.String("file", "", "absolute or cwd-relative path of the .ts file to transform")
61
78
  out := fs.String("out", "", "write output JS to PATH (default: stdout)")
62
79
  tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig owning --file")
63
80
  cwd := fs.String("cwd", "", "override the working directory")
64
81
  pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
82
+ projectContextJSON := fs.String("project-context-json", "", "ttsc project identity JSON")
65
83
  singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
66
84
  checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
67
85
  tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
@@ -71,25 +89,34 @@ func RunTransform(args []string) int {
71
89
  return 2
72
90
  }
73
91
  if *file == "" {
74
- fmt.Fprintln(os.Stderr, "@ttsc/lint transform: --file is required")
92
+ fmt.Fprintln(stderr, "@ttsc/lint transform: --file is required")
75
93
  return 2
76
94
  }
77
95
  tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
78
96
  if err != nil {
79
- fmt.Fprintln(os.Stderr, err)
97
+ fmt.Fprintln(stderr, err)
80
98
  return 2
81
99
  }
82
100
  resolvedCwd, err := resolveCwd(*cwd)
83
101
  if err != nil {
84
- fmt.Fprintln(os.Stderr, err)
102
+ fmt.Fprintln(stderr, err)
103
+ return 2
104
+ }
105
+ projectIdentity, err := decodeProjectIdentity(*projectContextJSON)
106
+ if err != nil {
107
+ fmt.Fprintln(stderr, err)
85
108
  return 2
86
109
  }
87
110
  rules, err := loadRules(*pluginsJSON, resolvedCwd, *tsconfig)
88
111
  if err != nil {
89
- fmt.Fprintln(os.Stderr, err)
112
+ fmt.Fprintln(stderr, err)
90
113
  return 2
91
114
  }
92
115
  engine := NewEngineWithResolver(rules)
116
+ if err := engine.ConfigError(); err != nil {
117
+ fmt.Fprintln(stderr, err)
118
+ return 2
119
+ }
93
120
  engine.SetSerial(*singleThreaded)
94
121
 
95
122
  prog, parseDiags, err := loadProgram(resolvedCwd, *tsconfig, loadProgramOptions{
@@ -98,24 +125,25 @@ func RunTransform(args []string) int {
98
125
  singleThreaded: *singleThreaded,
99
126
  checkers: *checkers,
100
127
  tsgoArgs: tsgoArgs,
128
+ projectIdentity: projectIdentity,
101
129
  })
102
130
  if err != nil {
103
- fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
131
+ fmt.Fprintf(stderr, "@ttsc/lint: %v\n", err)
104
132
  return 2
105
133
  }
106
134
  if len(parseDiags) > 0 {
107
- shimdw.FormatASTDiagnosticsWithColorAndContext(os.Stderr, parseDiags, resolvedCwd)
135
+ shimdw.FormatASTDiagnosticsWithColorAndContext(stderr, parseDiags, resolvedCwd)
108
136
  return 2
109
137
  }
110
138
  defer prog.close()
111
139
 
112
140
  astDiags, lintDiags, err := collectDiagnostics(prog, engine)
113
141
  if err != nil {
114
- fmt.Fprintln(os.Stderr, err)
142
+ fmt.Fprintln(stderr, err)
115
143
  return 2
116
144
  }
117
- warnUnknownRules(os.Stderr, engine.UnknownRules())
118
- if errors := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, resolvedCwd); errors > 0 {
145
+ warnUnknownRules(stderr, engine.UnknownRules())
146
+ if errors := shimdw.FormatMixedDiagnostics(stderr, astDiags, lintDiags, resolvedCwd); errors > 0 {
119
147
  return 2
120
148
  }
121
149
 
@@ -128,7 +156,7 @@ func RunTransform(args []string) int {
128
156
  absFile := shimtspath.ResolvePath(resolvedCwd, *file)
129
157
  target := prog.findSourceFile(absFile)
130
158
  if target == nil {
131
- fmt.Fprintf(os.Stderr, "@ttsc/lint transform: source file not in program: %s\n", absFile)
159
+ fmt.Fprintf(stderr, "@ttsc/lint transform: source file not in program: %s\n", absFile)
132
160
  return 2
133
161
  }
134
162
 
@@ -145,55 +173,69 @@ func RunTransform(args []string) int {
145
173
  WriteFile: shimcompiler.WriteFile(capture),
146
174
  })
147
175
  if result == nil {
148
- fmt.Fprintln(os.Stderr, "@ttsc/lint transform: Emit returned nil")
176
+ fmt.Fprintln(stderr, "@ttsc/lint transform: Emit returned nil")
149
177
  return 3
150
178
  }
151
179
  if len(result.Diagnostics) > 0 {
152
- shimdw.FormatASTDiagnosticsWithColorAndContext(os.Stderr, result.Diagnostics, resolvedCwd)
180
+ shimdw.FormatASTDiagnosticsWithColorAndContext(stderr, result.Diagnostics, resolvedCwd)
153
181
  }
154
182
  if captured == "" {
155
- fmt.Fprintf(os.Stderr, "@ttsc/lint transform: no output produced for %s\n", absFile)
183
+ fmt.Fprintf(stderr, "@ttsc/lint transform: no output produced for %s\n", absFile)
156
184
  return 3
157
185
  }
158
186
  if *out == "" {
159
- fmt.Fprint(os.Stdout, captured)
187
+ fmt.Fprint(stdout, captured)
160
188
  return 0
161
189
  }
162
190
  if err := os.MkdirAll(filepath.Dir(*out), 0o755); err != nil {
163
- fmt.Fprintf(os.Stderr, "@ttsc/lint transform: mkdir: %v\n", err)
191
+ fmt.Fprintf(stderr, "@ttsc/lint transform: mkdir: %v\n", err)
164
192
  return 3
165
193
  }
166
194
  if err := os.WriteFile(*out, []byte(captured), 0o644); err != nil {
167
- fmt.Fprintf(os.Stderr, "@ttsc/lint transform: write: %v\n", err)
195
+ fmt.Fprintf(stderr, "@ttsc/lint transform: write: %v\n", err)
168
196
  return 3
169
197
  }
170
198
  return 0
171
199
  }
172
200
 
173
201
  type subcommandOpts struct {
174
- cwd string
175
- tsconfig string
176
- pluginsJSON string
177
- emit bool
178
- noEmit bool
179
- quiet bool
180
- verbose bool
181
- diagnostics bool
182
- outDir string
183
- singleThreaded bool
184
- checkers int
185
- tsgoArgs []string
202
+ cwd string
203
+ tsconfig string
204
+ pluginsJSON string
205
+ emit bool
206
+ noEmit bool
207
+ quiet bool
208
+ verbose bool
209
+ diagnostics bool
210
+ outDir string
211
+ singleThreaded bool
212
+ checkers int
213
+ tsgoArgs []string
214
+ projectIdentity publicrule.ProjectIdentity
215
+ stdout io.Writer
216
+ stderr io.Writer
186
217
  }
187
218
 
188
219
  // parseSubcommandFlags parses the shared flag set used by the `check`,
189
220
  // `build`, and `fix`/`format` subcommands. Unknown flags are silently
190
221
  // stripped by `filterKnownFlags` before the standard FlagSet sees them.
191
222
  func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
223
+ return parseSubcommandFlagsWithIO(name, args, os.Stdout, os.Stderr)
224
+ }
225
+
226
+ func parseSubcommandFlagsWithIO(name string, args []string, stdout, stderr io.Writer) (*subcommandOpts, error) {
227
+ if stdout == nil {
228
+ stdout = io.Discard
229
+ }
230
+ if stderr == nil {
231
+ stderr = io.Discard
232
+ }
192
233
  fs := flag.NewFlagSet(name, flag.ContinueOnError)
193
- fs.SetOutput(os.Stderr)
234
+ fs.SetOutput(stderr)
194
235
  cwd := fs.String("cwd", "", "")
195
236
  tsconfig := fs.String("tsconfig", "tsconfig.json", "")
196
237
  pluginsJSON := fs.String("plugins-json", "", "")
238
+ projectContextJSON := fs.String("project-context-json", "", "")
197
239
  emit := fs.Bool("emit", false, "")
198
240
  noEmit := fs.Bool("noEmit", false, "")
199
241
  quiet := fs.Bool("quiet", false, "")
@@ -218,19 +260,26 @@ func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
218
260
  if err != nil {
219
261
  return nil, err
220
262
  }
263
+ projectIdentity, err := decodeProjectIdentity(*projectContextJSON)
264
+ if err != nil {
265
+ return nil, err
266
+ }
221
267
  return &subcommandOpts{
222
- cwd: resolvedCwd,
223
- tsconfig: *tsconfig,
224
- pluginsJSON: *pluginsJSON,
225
- emit: *emit,
226
- noEmit: *noEmit,
227
- quiet: *quiet,
228
- verbose: *verbose,
229
- diagnostics: *diagnostics || *extendedDiagnostics,
230
- outDir: *outDir,
231
- singleThreaded: *singleThreaded,
232
- checkers: *checkers,
233
- tsgoArgs: tsgoArgs,
268
+ cwd: resolvedCwd,
269
+ tsconfig: *tsconfig,
270
+ pluginsJSON: *pluginsJSON,
271
+ emit: *emit,
272
+ noEmit: *noEmit,
273
+ quiet: *quiet,
274
+ verbose: *verbose,
275
+ diagnostics: *diagnostics || *extendedDiagnostics,
276
+ outDir: *outDir,
277
+ singleThreaded: *singleThreaded,
278
+ checkers: *checkers,
279
+ tsgoArgs: tsgoArgs,
280
+ projectIdentity: projectIdentity,
281
+ stdout: stdout,
282
+ stderr: stderr,
234
283
  }, nil
235
284
  }
236
285
 
@@ -254,10 +303,14 @@ func decodeTsgoArgs(raw string) ([]string, error) {
254
303
  func runProject(opts *subcommandOpts) int {
255
304
  rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
256
305
  if err != nil {
257
- fmt.Fprintln(os.Stderr, err)
306
+ fmt.Fprintln(opts.stderr, err)
258
307
  return 2
259
308
  }
260
309
  engine := NewEngineWithResolver(rules)
310
+ if err := engine.ConfigError(); err != nil {
311
+ fmt.Fprintln(opts.stderr, err)
312
+ return 2
313
+ }
261
314
  engine.SetSerial(opts.singleThreaded)
262
315
 
263
316
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
@@ -268,25 +321,26 @@ func runProject(opts *subcommandOpts) int {
268
321
  singleThreaded: opts.singleThreaded,
269
322
  checkers: opts.checkers,
270
323
  tsgoArgs: opts.tsgoArgs,
324
+ projectIdentity: opts.projectIdentity,
271
325
  })
272
326
  if err != nil {
273
- fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
327
+ fmt.Fprintf(opts.stderr, "@ttsc/lint: %v\n", err)
274
328
  return 2
275
329
  }
276
330
  if len(parseDiags) > 0 {
277
- shimdw.FormatASTDiagnosticsWithColorAndContext(os.Stderr, parseDiags, opts.cwd)
331
+ shimdw.FormatASTDiagnosticsWithColorAndContext(opts.stderr, parseDiags, opts.cwd)
278
332
  return 2
279
333
  }
280
334
  defer prog.close()
281
335
 
282
336
  astDiags, lintDiags, diagnosticsTiming, err := collectDiagnosticsTimed(prog, engine)
283
337
  if err != nil {
284
- fmt.Fprintln(os.Stderr, err)
338
+ fmt.Fprintln(opts.stderr, err)
285
339
  return 2
286
340
  }
287
- printLintDiagnosticsTiming(os.Stdout, opts.diagnostics, diagnosticsTiming)
288
- warnUnknownRules(os.Stderr, engine.UnknownRules())
289
- if errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
341
+ printLintDiagnosticsTiming(opts.stdout, opts.diagnostics, diagnosticsTiming)
342
+ warnUnknownRules(opts.stderr, engine.UnknownRules())
343
+ if errCount := shimdw.FormatMixedDiagnostics(opts.stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
290
344
  return 2
291
345
  }
292
346
 
@@ -300,19 +354,19 @@ func runProject(opts *subcommandOpts) int {
300
354
  }),
301
355
  })
302
356
  if result == nil {
303
- fmt.Fprintln(os.Stderr, "@ttsc/lint: Emit returned nil")
357
+ fmt.Fprintln(opts.stderr, "@ttsc/lint: Emit returned nil")
304
358
  return 3
305
359
  }
306
360
  if len(result.Diagnostics) > 0 {
307
- errCount := shimdw.FormatMixedDiagnostics(os.Stderr, result.Diagnostics, nil, opts.cwd)
361
+ errCount := shimdw.FormatMixedDiagnostics(opts.stderr, result.Diagnostics, nil, opts.cwd)
308
362
  if errCount > 0 {
309
363
  return 2
310
364
  }
311
365
  }
312
366
  if opts.verbose && result.EmittedFiles != nil {
313
- fmt.Fprintf(os.Stdout, "@ttsc/lint: emitted=%d files\n", len(result.EmittedFiles))
367
+ fmt.Fprintf(opts.stdout, "@ttsc/lint: emitted=%d files\n", len(result.EmittedFiles))
314
368
  for _, f := range result.EmittedFiles {
315
- fmt.Fprintln(os.Stdout, " +", f)
369
+ fmt.Fprintln(opts.stdout, " +", f)
316
370
  }
317
371
  }
318
372
  return 0
@@ -331,9 +385,24 @@ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
331
385
  return nil, err
332
386
  }
333
387
  if entry == nil {
334
- return RuleConfig{}, nil
388
+ return bindProjectRuleResolver(RuleConfig{})
389
+ }
390
+ resolver, err := LoadConfigResolver(entry, cwd, tsconfigPath)
391
+ if err != nil {
392
+ return nil, err
335
393
  }
336
- return LoadConfigResolver(entry, cwd, tsconfigPath)
394
+ return bindProjectRuleResolver(resolver)
395
+ }
396
+
397
+ func decodeProjectIdentity(raw string) (publicrule.ProjectIdentity, error) {
398
+ if strings.TrimSpace(raw) == "" {
399
+ return publicrule.ProjectIdentity{}, nil
400
+ }
401
+ var identity publicrule.ProjectIdentity
402
+ if err := json.Unmarshal([]byte(raw), &identity); err != nil {
403
+ return publicrule.ProjectIdentity{}, fmt.Errorf("@ttsc/lint: invalid --project-context-json: %w", err)
404
+ }
405
+ return identity, nil
337
406
  }
338
407
 
339
408
  // warnUnknownRules writes one warning line per name in `unknown` to `w`.
@@ -396,9 +465,8 @@ type lintDiagnosticsTiming struct {
396
465
  func collectDiagnosticsTimed(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, lintDiagnosticsTiming, error) {
397
466
  timing := lintDiagnosticsTiming{}
398
467
  astDiags := prog.programDiagnostics()
399
- files := prog.userSourceFiles()
400
468
  lintStarted := time.Now()
401
- findings := engine.Run(files, prog.checker)
469
+ findings := prog.runLintCycle(engine)
402
470
  timing.lint = time.Since(lintStarted)
403
471
  nativeDiags := make([]*shimdw.LintDiagnostic, 0, len(findings))
404
472
  for _, finding := range findings {
@@ -429,24 +497,6 @@ func formatTimingSeconds(duration time.Duration) string {
429
497
  return fmt.Sprintf("%.3fs", duration.Seconds())
430
498
  }
431
499
 
432
- // RuleCode hashes a rule name into a stable, positive int32 so the
433
- // renderer's banner (`TS9123`-style) is unique per rule. Codes start at
434
- // 9000 to avoid colliding with tsgo's diagnostic codes (which top out
435
- // well below that range). Public because tests pin the hash band.
436
- func RuleCode(name string) int32 {
437
- const prime = 16777619
438
- var h uint32 = 2166136261
439
- for i := 0; i < len(name); i++ {
440
- h ^= uint32(name[i])
441
- h *= prime
442
- }
443
- return int32(9000 + (h % 9000))
444
- }
445
-
446
- // ruleCode is the internal alias kept for callers in the same package
447
- // that prefer the lowercase form.
448
- func ruleCode(name string) int32 { return RuleCode(name) }
449
-
450
500
  // resolveCwd returns an absolute working directory. When `override` is
451
501
  // non-empty it is made absolute; otherwise the process working directory
452
502
  // is returned.