@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.
- package/README.md +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- 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
|
+
}
|
package/linthost/compile.go
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
131
|
+
fmt.Fprintf(stderr, "@ttsc/lint: %v\n", err)
|
|
104
132
|
return 2
|
|
105
133
|
}
|
|
106
134
|
if len(parseDiags) > 0 {
|
|
107
|
-
shimdw.FormatASTDiagnosticsWithColorAndContext(
|
|
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(
|
|
142
|
+
fmt.Fprintln(stderr, err)
|
|
115
143
|
return 2
|
|
116
144
|
}
|
|
117
|
-
warnUnknownRules(
|
|
118
|
-
if errors := shimdw.FormatMixedDiagnostics(
|
|
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(
|
|
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(
|
|
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(
|
|
180
|
+
shimdw.FormatASTDiagnosticsWithColorAndContext(stderr, result.Diagnostics, resolvedCwd)
|
|
153
181
|
}
|
|
154
182
|
if captured == "" {
|
|
155
|
-
fmt.Fprintf(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
175
|
-
tsconfig
|
|
176
|
-
pluginsJSON
|
|
177
|
-
emit
|
|
178
|
-
noEmit
|
|
179
|
-
quiet
|
|
180
|
-
verbose
|
|
181
|
-
diagnostics
|
|
182
|
-
outDir
|
|
183
|
-
singleThreaded
|
|
184
|
-
checkers
|
|
185
|
-
tsgoArgs
|
|
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(
|
|
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:
|
|
223
|
-
tsconfig:
|
|
224
|
-
pluginsJSON:
|
|
225
|
-
emit:
|
|
226
|
-
noEmit:
|
|
227
|
-
quiet:
|
|
228
|
-
verbose:
|
|
229
|
-
diagnostics:
|
|
230
|
-
outDir:
|
|
231
|
-
singleThreaded:
|
|
232
|
-
checkers:
|
|
233
|
-
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(
|
|
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(
|
|
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(
|
|
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(
|
|
338
|
+
fmt.Fprintln(opts.stderr, err)
|
|
285
339
|
return 2
|
|
286
340
|
}
|
|
287
|
-
printLintDiagnosticsTiming(
|
|
288
|
-
warnUnknownRules(
|
|
289
|
-
if errCount := shimdw.FormatMixedDiagnostics(
|
|
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(
|
|
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(
|
|
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(
|
|
367
|
+
fmt.Fprintf(opts.stdout, "@ttsc/lint: emitted=%d files\n", len(result.EmittedFiles))
|
|
314
368
|
for _, f := range result.EmittedFiles {
|
|
315
|
-
fmt.Fprintln(
|
|
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{}
|
|
388
|
+
return bindProjectRuleResolver(RuleConfig{})
|
|
389
|
+
}
|
|
390
|
+
resolver, err := LoadConfigResolver(entry, cwd, tsconfigPath)
|
|
391
|
+
if err != nil {
|
|
392
|
+
return nil, err
|
|
335
393
|
}
|
|
336
|
-
return
|
|
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 :=
|
|
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.
|