@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
@@ -1,26 +1,931 @@
1
- // unicorn/template-indent: re-indents the contents of tagged template
2
- // literals (`outdent`, `dedent`, `stripIndent`, etc.) so the inner text
3
- // is column-aligned with the surrounding code. The autofix has to
4
- // detect the common leading-whitespace prefix of every line in the
5
- // template body, decide what the desired indent level is from the
6
- // template's own column, then rewrite every interior line — substantial
7
- // whitespace analysis well beyond a pattern match.
1
+ // unicorn/template-indent normalizes whitespace-insensitive template bodies
2
+ // selected by tag, call argument, leading block comment, AST selector, or the
3
+ // Jest inline-snapshot shape. Edits target only the raw TemplateHead / Middle /
4
+ // Tail payloads, so substitutions, nested expressions, and escape spelling are
5
+ // preserved byte-for-byte.
8
6
  //
9
- // Registered as a no-op stub to keep
10
- // `TestTypedKeysMatchRegisteredRules` aligned with the typed
11
- // `ITtscLintUnicornRules` surface. The matching fixture under
12
- // `tests/test-lint/src/cases/unicorn-template-indent.ts` keeps its
13
- // `@ttsc-corpus-skip` directive.
14
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/template-indent.md
7
+ // The indentation pipeline mirrors the current upstream rule: remove the
8
+ // common body margin, trim only the boundary line breaks, then indent every
9
+ // non-empty line relative to the source line that contains the opening
10
+ // backtick. Source-wide indent detection excludes the interior lines of every
11
+ // template so template data cannot decide the surrounding code style.
12
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/template-indent.js
15
13
  package linthost
16
14
 
17
- import shimast "github.com/microsoft/typescript-go/shim/ast"
15
+ import (
16
+ "bytes"
17
+ "encoding/json"
18
+ "fmt"
19
+ "sort"
20
+ "strings"
21
+ "unicode"
22
+ "unicode/utf8"
18
23
 
19
- type unicornTemplateIndent struct{}
24
+ shimast "github.com/microsoft/typescript-go/shim/ast"
25
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
26
+ )
20
27
 
21
- func (unicornTemplateIndent) Name() string { return "unicorn/template-indent" }
22
- func (unicornTemplateIndent) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
23
- func (unicornTemplateIndent) Check(_ *Context, _ *shimast.Node) {
28
+ const unicornTemplateIndentMessage = "Templates should be properly indented."
29
+
30
+ var (
31
+ unicornTemplateIndentDefaultTags = []string{
32
+ "outdent", "dedent", "gql", "sql", "html", "styled",
33
+ }
34
+ unicornTemplateIndentDefaultFunctions = []string{"dedent", "stripIndent"}
35
+ unicornTemplateIndentDefaultComments = []string{"HTML", "indent"}
36
+ )
37
+
38
+ type unicornTemplateIndent struct{ optionsRule }
39
+
40
+ type unicornTemplateIndentRawOptions struct {
41
+ Indent json.RawMessage `json:"indent"`
42
+ Tags json.RawMessage `json:"tags"`
43
+ Functions json.RawMessage `json:"functions"`
44
+ Selectors json.RawMessage `json:"selectors"`
45
+ Comments json.RawMessage `json:"comments"`
46
+ }
47
+
48
+ type unicornTemplateIndentOptions struct {
49
+ indent string
50
+ indentSet bool
51
+ tags []string
52
+ functions []string
53
+ selectors []*astSelector
54
+ comments []string
55
+ }
56
+
57
+ type unicornTemplateIndentQuasi struct {
58
+ pos int
59
+ end int
60
+ text string
61
+ }
62
+
63
+ type unicornTemplateIndentStat struct {
64
+ kind byte
65
+ amount int
66
+ uses int
67
+ weight int
68
+ }
69
+
70
+ type unicornTemplateIndentLine struct {
71
+ text string
72
+ terminator string
73
+ }
74
+
75
+ func (unicornTemplateIndent) Name() string { return "unicorn/template-indent" }
76
+ func (unicornTemplateIndent) Visits() []shimast.Kind {
77
+ return []shimast.Kind{shimast.KindSourceFile}
78
+ }
79
+
80
+ func (unicornTemplateIndent) ValidateOptions(raw json.RawMessage) error {
81
+ _, err := compileUnicornTemplateIndentOptions(raw)
82
+ return err
83
+ }
84
+
85
+ func (unicornTemplateIndent) Check(ctx *Context, root *shimast.Node) {
86
+ if ctx == nil || ctx.File == nil || root == nil {
87
+ return
88
+ }
89
+ options, err := compileUnicornTemplateIndentOptions(ctx.Options)
90
+ if err != nil {
91
+ // Engine construction reports malformed options as a configuration error.
92
+ // Direct contributor calls that bypass construction remain side-effect-free.
93
+ return
94
+ }
95
+
96
+ source := ctx.File.Text()
97
+ templates := collectUnicornTemplateIndentTemplates(root)
98
+ commentFactory := shimast.NewNodeFactory(shimast.NodeFactoryHooks{})
99
+ selected := make(map[*shimast.Node]struct{})
100
+ for _, selector := range options.selectors {
101
+ for _, match := range matchASTSelector(root, selector) {
102
+ if isUnicornTemplateIndentLiteral(match) {
103
+ selected[match] = struct{}{}
104
+ }
105
+ }
106
+ }
107
+ ignoredLines := unicornTemplateIndentIgnoredLines(source, templates)
108
+
109
+ for _, template := range templates {
110
+ if !unicornTemplateIndentShouldCheck(ctx.File, commentFactory, source, template, options, selected) {
111
+ continue
112
+ }
113
+ edits, changed := unicornTemplateIndentEdits(source, template, options, ignoredLines)
114
+ if !changed {
115
+ continue
116
+ }
117
+ ctx.ReportFix(template, unicornTemplateIndentMessage, edits...)
118
+ }
119
+ }
120
+
121
+ func compileUnicornTemplateIndentOptions(raw json.RawMessage) (unicornTemplateIndentOptions, error) {
122
+ options := unicornTemplateIndentOptions{
123
+ tags: append([]string(nil), unicornTemplateIndentDefaultTags...),
124
+ functions: append([]string(nil), unicornTemplateIndentDefaultFunctions...),
125
+ comments: append([]string(nil), unicornTemplateIndentDefaultComments...),
126
+ }
127
+ raw = bytes.TrimSpace(raw)
128
+ if len(raw) == 0 {
129
+ for index := range options.comments {
130
+ options.comments[index] = strings.ToLower(options.comments[index])
131
+ }
132
+ return options, nil
133
+ }
134
+ if raw[0] != '{' {
135
+ return unicornTemplateIndentOptions{}, fmt.Errorf("unicorn/template-indent options must be an object")
136
+ }
137
+
138
+ encoded := unicornTemplateIndentRawOptions{}
139
+ if err := decodeStrictJSON(raw, &encoded); err != nil {
140
+ return unicornTemplateIndentOptions{}, fmt.Errorf("unicorn/template-indent options must contain only indent, tags, functions, selectors, and comments: %w", err)
141
+ }
142
+
143
+ if len(encoded.Indent) > 0 {
144
+ indent, err := decodeUnicornTemplateIndentIndent(encoded.Indent)
145
+ if err != nil {
146
+ return unicornTemplateIndentOptions{}, err
147
+ }
148
+ options.indent = indent
149
+ options.indentSet = true
150
+ }
151
+
152
+ arrays := []struct {
153
+ name string
154
+ raw json.RawMessage
155
+ defaults []string
156
+ target *[]string
157
+ }{
158
+ {name: "tags", raw: encoded.Tags, defaults: options.tags, target: &options.tags},
159
+ {name: "functions", raw: encoded.Functions, defaults: options.functions, target: &options.functions},
160
+ {name: "selectors", raw: encoded.Selectors, target: nil},
161
+ {name: "comments", raw: encoded.Comments, defaults: options.comments, target: &options.comments},
162
+ }
163
+ var selectorSources []string
164
+ for _, entry := range arrays {
165
+ values, err := decodeUnicornTemplateIndentStrings(entry.name, entry.raw, entry.defaults)
166
+ if err != nil {
167
+ return unicornTemplateIndentOptions{}, err
168
+ }
169
+ if entry.name == "selectors" {
170
+ selectorSources = values
171
+ continue
172
+ }
173
+ *entry.target = values
174
+ }
175
+
176
+ options.selectors = make([]*astSelector, 0, len(selectorSources))
177
+ for index, source := range selectorSources {
178
+ selector, err := parseASTSelector(source)
179
+ if err != nil {
180
+ return unicornTemplateIndentOptions{}, fmt.Errorf("unicorn/template-indent selector %d %q is invalid: %w", index+1, source, err)
181
+ }
182
+ options.selectors = append(options.selectors, selector)
183
+ }
184
+ for index := range options.comments {
185
+ options.comments[index] = strings.ToLower(options.comments[index])
186
+ }
187
+ return options, nil
188
+ }
189
+
190
+ func decodeUnicornTemplateIndentIndent(raw json.RawMessage) (string, error) {
191
+ raw = bytes.TrimSpace(raw)
192
+ if len(raw) == 0 {
193
+ return "", fmt.Errorf("unicorn/template-indent indent must be a positive integer or non-empty whitespace string")
194
+ }
195
+ if raw[0] == '"' {
196
+ var indent string
197
+ if err := decodeStrictJSON(raw, &indent); err != nil {
198
+ return "", fmt.Errorf("unicorn/template-indent indent must be a string: %w", err)
199
+ }
200
+ if indent == "" {
201
+ return "", fmt.Errorf("unicorn/template-indent indent string must not be empty")
202
+ }
203
+ for _, character := range indent {
204
+ if !unicornTemplateIndentIsECMAScriptWhitespace(character) {
205
+ return "", fmt.Errorf("unicorn/template-indent indent string must contain only whitespace")
206
+ }
207
+ }
208
+ return indent, nil
209
+ }
210
+
211
+ var count json.Number
212
+ if err := decodeStrictJSON(raw, &count); err != nil {
213
+ return "", fmt.Errorf("unicorn/template-indent indent must be a positive integer or non-empty whitespace string: %w", err)
214
+ }
215
+ value, err := count.Int64()
216
+ if err != nil || value < 1 || uint64(value) > uint64(^uint(0)>>1) {
217
+ return "", fmt.Errorf("unicorn/template-indent indent number must be a positive integer")
218
+ }
219
+ return strings.Repeat(" ", int(value)), nil
220
+ }
221
+
222
+ func decodeUnicornTemplateIndentStrings(name string, raw json.RawMessage, defaults []string) ([]string, error) {
223
+ raw = bytes.TrimSpace(raw)
224
+ if len(raw) == 0 {
225
+ return append([]string(nil), defaults...), nil
226
+ }
227
+ if raw[0] != '[' {
228
+ return nil, fmt.Errorf("unicorn/template-indent %s must be an array of unique strings", name)
229
+ }
230
+ var values []string
231
+ if err := decodeStrictJSON(raw, &values); err != nil {
232
+ return nil, fmt.Errorf("unicorn/template-indent %s must be an array of unique strings: %w", name, err)
233
+ }
234
+ seen := make(map[string]struct{}, len(values))
235
+ for _, value := range values {
236
+ if _, duplicate := seen[value]; duplicate {
237
+ return nil, fmt.Errorf("unicorn/template-indent %s must not contain duplicate %q", name, value)
238
+ }
239
+ seen[value] = struct{}{}
240
+ }
241
+ return values, nil
242
+ }
243
+
244
+ func collectUnicornTemplateIndentTemplates(root *shimast.Node) []*shimast.Node {
245
+ templates := make([]*shimast.Node, 0)
246
+ walkDescendants(root, func(node *shimast.Node) {
247
+ if isUnicornTemplateIndentLiteral(node) {
248
+ templates = append(templates, node)
249
+ }
250
+ })
251
+ return templates
252
+ }
253
+
254
+ func isUnicornTemplateIndentLiteral(node *shimast.Node) bool {
255
+ return node != nil && (node.Kind == shimast.KindNoSubstitutionTemplateLiteral || node.Kind == shimast.KindTemplateExpression)
256
+ }
257
+
258
+ func unicornTemplateIndentShouldCheck(
259
+ file *shimast.SourceFile,
260
+ commentFactory *shimast.NodeFactory,
261
+ source string,
262
+ template *shimast.Node,
263
+ options unicornTemplateIndentOptions,
264
+ selected map[*shimast.Node]struct{},
265
+ ) bool {
266
+ if _, ok := selected[template]; ok {
267
+ return true
268
+ }
269
+ start := shimscanner.SkipTrivia(source, template.Pos())
270
+ if unicornTemplateIndentCommentMatches(commentFactory, source, template.Pos(), start, options.comments) {
271
+ return true
272
+ }
273
+ if unicornTemplateIndentIsJestInlineSnapshot(template) {
274
+ return true
275
+ }
276
+
277
+ argument := unicornTemplateIndentOuterParenthesizedExpression(template)
278
+ parent := argument.Parent
279
+ if parent == nil {
280
+ return false
281
+ }
282
+ if parent.Kind == shimast.KindTaggedTemplateExpression {
283
+ tagged := parent.AsTaggedTemplateExpression()
284
+ return tagged != nil && tagged.Template == template &&
285
+ unicornTemplateIndentMatchesName(file, tagged.Tag, options.tags)
286
+ }
287
+ if parent.Kind != shimast.KindCallExpression {
288
+ return false
289
+ }
290
+ call := parent.AsCallExpression()
291
+ if call == nil || call.Arguments == nil || !unicornTemplateIndentContainsNode(call.Arguments.Nodes, argument) {
292
+ return false
293
+ }
294
+ return unicornTemplateIndentMatchesName(file, call.Expression, options.functions)
295
+ }
296
+
297
+ func unicornTemplateIndentMatchesName(file *shimast.SourceFile, node *shimast.Node, names []string) bool {
298
+ path := unicornTemplateIndentNamePath(file, node)
299
+ if path == "" {
300
+ return false
301
+ }
302
+ for _, name := range names {
303
+ if path == unicornTemplateIndentTrim(name) {
304
+ return true
305
+ }
306
+ }
307
+ return false
308
+ }
309
+
310
+ func unicornTemplateIndentNamePath(file *shimast.SourceFile, node *shimast.Node) string {
311
+ node = stripParens(node)
312
+ if node == nil {
313
+ return ""
314
+ }
315
+ switch node.Kind {
316
+ case shimast.KindIdentifier:
317
+ return identifierText(node)
318
+ case shimast.KindThisKeyword:
319
+ return "this"
320
+ case shimast.KindSuperKeyword:
321
+ return "super"
322
+ case shimast.KindMetaProperty:
323
+ text := nodeText(file, node)
324
+ if text == "import.meta" || text == "new.target" {
325
+ return text
326
+ }
327
+ case shimast.KindPropertyAccessExpression:
328
+ access := node.AsPropertyAccessExpression()
329
+ if access == nil || access.QuestionDotToken != nil || access.Name() == nil || access.Name().Kind != shimast.KindIdentifier {
330
+ return ""
331
+ }
332
+ object := unicornTemplateIndentNamePath(file, access.Expression)
333
+ if object == "" {
334
+ return ""
335
+ }
336
+ return object + "." + identifierText(access.Name())
337
+ }
338
+ return ""
339
+ }
340
+
341
+ func unicornTemplateIndentContainsNode(nodes []*shimast.Node, target *shimast.Node) bool {
342
+ for _, node := range nodes {
343
+ if node == target {
344
+ return true
345
+ }
346
+ }
347
+ return false
348
+ }
349
+
350
+ func unicornTemplateIndentIsJestInlineSnapshot(template *shimast.Node) bool {
351
+ template = unicornTemplateIndentOuterParenthesizedExpression(template)
352
+ if template == nil || template.Parent == nil || template.Parent.Kind != shimast.KindCallExpression {
353
+ return false
354
+ }
355
+ snapshotCall := template.Parent.AsCallExpression()
356
+ if snapshotCall == nil || snapshotCall.QuestionDotToken != nil || snapshotCall.Arguments == nil ||
357
+ len(snapshotCall.Arguments.Nodes) != 1 || snapshotCall.Arguments.Nodes[0] != template {
358
+ return false
359
+ }
360
+ if snapshotCall.Expression == nil {
361
+ return false
362
+ }
363
+ memberNode := stripParens(snapshotCall.Expression)
364
+ if memberNode == nil || memberNode.Kind != shimast.KindPropertyAccessExpression {
365
+ return false
366
+ }
367
+ member := memberNode.AsPropertyAccessExpression()
368
+ if member == nil || member.QuestionDotToken != nil || identifierText(member.Name()) != "toMatchInlineSnapshot" {
369
+ return false
370
+ }
371
+ if member.Expression == nil {
372
+ return false
373
+ }
374
+ expectNode := stripParens(member.Expression)
375
+ if expectNode == nil || expectNode.Kind != shimast.KindCallExpression {
376
+ return false
377
+ }
378
+ expectCall := expectNode.AsCallExpression()
379
+ return expectCall != nil && expectCall.QuestionDotToken == nil && expectCall.Arguments != nil &&
380
+ len(expectCall.Arguments.Nodes) == 1 && identifierText(stripParens(expectCall.Expression)) == "expect"
381
+ }
382
+
383
+ func unicornTemplateIndentOuterParenthesizedExpression(node *shimast.Node) *shimast.Node {
384
+ for node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindParenthesizedExpression {
385
+ parenthesized := node.Parent.AsParenthesizedExpression()
386
+ if parenthesized == nil || parenthesized.Expression != node {
387
+ break
388
+ }
389
+ node = node.Parent
390
+ }
391
+ return node
392
+ }
393
+
394
+ func unicornTemplateIndentCommentMatches(
395
+ factory *shimast.NodeFactory,
396
+ source string,
397
+ triviaStart int,
398
+ tokenStart int,
399
+ comments []string,
400
+ ) bool {
401
+ if factory == nil || len(comments) == 0 || triviaStart < 0 ||
402
+ tokenStart <= triviaStart || tokenStart > len(source) {
403
+ return false
404
+ }
405
+
406
+ var previous shimast.CommentRange
407
+ found := false
408
+ consider := func(comment shimast.CommentRange) {
409
+ if comment.Pos() < triviaStart || comment.End() > tokenStart {
410
+ return
411
+ }
412
+ if !found || comment.End() > previous.End() {
413
+ previous = comment
414
+ found = true
415
+ }
416
+ }
417
+ for comment := range shimscanner.GetTrailingCommentRanges(factory, source, triviaStart) {
418
+ consider(comment)
419
+ }
420
+ for comment := range shimscanner.GetLeadingCommentRanges(factory, source, triviaStart) {
421
+ consider(comment)
422
+ }
423
+ if !found || previous.Kind != shimast.KindMultiLineCommentTrivia ||
424
+ previous.Pos()+4 > previous.End() || previous.End() > len(source) {
425
+ return false
426
+ }
427
+ value := strings.ToLower(unicornTemplateIndentTrim(source[previous.Pos()+2 : previous.End()-2]))
428
+ for _, comment := range comments {
429
+ if value == comment {
430
+ return true
431
+ }
432
+ }
433
+ return false
434
+ }
435
+
436
+ func unicornTemplateIndentEdits(
437
+ source string,
438
+ template *shimast.Node,
439
+ options unicornTemplateIndentOptions,
440
+ ignoredLines map[int]struct{},
441
+ ) ([]TextEdit, bool) {
442
+ quasis, ok := unicornTemplateIndentQuasis(source, template)
443
+ if !ok || len(quasis) == 0 {
444
+ return nil, false
445
+ }
446
+ marker := unicornTemplateIndentMarker(quasis)
447
+ var joinedBuilder strings.Builder
448
+ for index, quasi := range quasis {
449
+ if index > 0 {
450
+ joinedBuilder.WriteString(marker)
451
+ }
452
+ joinedBuilder.WriteString(quasi.text)
453
+ }
454
+ joined := joinedBuilder.String()
455
+ eol := unicornTemplateIndentFirstEOL(joined)
456
+ if eol == "" {
457
+ return nil, false
458
+ }
459
+
460
+ templateStart := shimscanner.SkipTrivia(source, template.Pos())
461
+ parentMargin := unicornTemplateIndentParentMargin(source, templateStart)
462
+ indent := options.indent
463
+ if !options.indentSet {
464
+ if parentMargin == "" {
465
+ indent = unicornTemplateIndentDetect(
466
+ unicornTemplateIndentSourceForDetection(source, ignoredLines),
467
+ )
468
+ if indent == "" {
469
+ templateText := unicornTemplateIndentTextForDetection(joined)
470
+ indent = unicornTemplateIndentDetect(unicornTemplateIndentStrip(templateText))
471
+ if indent == "" {
472
+ indent = unicornTemplateIndentDetect(templateText)
473
+ }
474
+ }
475
+ if indent == "" {
476
+ indent = " "
477
+ }
478
+ } else if parentMargin[0] == '\t' {
479
+ indent = "\t"
480
+ } else {
481
+ indent = " "
482
+ }
483
+ }
484
+
485
+ fixed := unicornTemplateIndentStrip(joined)
486
+ if strings.HasPrefix(fixed, eol) {
487
+ fixed = fixed[len(eol):]
488
+ }
489
+ if boundary := strings.LastIndex(fixed, eol); boundary >= 0 &&
490
+ unicornTemplateIndentOnlyHorizontalSpace(fixed[boundary+len(eol):]) {
491
+ fixed = fixed[:boundary]
492
+ }
493
+ fixed = unicornTemplateIndentIndentNonEmpty(fixed, parentMargin+indent)
494
+ fixed = eol + fixed + eol + parentMargin
495
+ if fixed == joined {
496
+ return nil, false
497
+ }
498
+
499
+ replacements := strings.Split(fixed, marker)
500
+ if len(replacements) != len(quasis) {
501
+ return nil, false
502
+ }
503
+ edits := make([]TextEdit, 0, len(quasis))
504
+ for index, quasi := range quasis {
505
+ if replacements[index] == quasi.text {
506
+ continue
507
+ }
508
+ edits = append(edits, TextEdit{Pos: quasi.pos, End: quasi.end, Text: replacements[index]})
509
+ }
510
+ return edits, len(edits) > 0
511
+ }
512
+
513
+ func unicornTemplateIndentQuasis(source string, template *shimast.Node) ([]unicornTemplateIndentQuasi, bool) {
514
+ if template == nil {
515
+ return nil, false
516
+ }
517
+ switch template.Kind {
518
+ case shimast.KindNoSubstitutionTemplateLiteral:
519
+ start := shimscanner.SkipTrivia(source, template.Pos())
520
+ end := template.End()
521
+ if start < 0 || end > len(source) || start+2 > end || source[start] != '`' || source[end-1] != '`' {
522
+ return nil, false
523
+ }
524
+ return []unicornTemplateIndentQuasi{{pos: start + 1, end: end - 1, text: source[start+1 : end-1]}}, true
525
+ case shimast.KindTemplateExpression:
526
+ expression := template.AsTemplateExpression()
527
+ if expression == nil || expression.Head == nil || expression.TemplateSpans == nil {
528
+ return nil, false
529
+ }
530
+ headStart := shimscanner.SkipTrivia(source, expression.Head.Pos())
531
+ headEnd := expression.Head.End()
532
+ if headStart < 0 || headEnd > len(source) || headStart+3 > headEnd || source[headStart] != '`' || source[headEnd-2:headEnd] != "${" {
533
+ return nil, false
534
+ }
535
+ quasis := []unicornTemplateIndentQuasi{{
536
+ pos: headStart + 1,
537
+ end: headEnd - 2,
538
+ text: source[headStart+1 : headEnd-2],
539
+ }}
540
+ for _, spanNode := range expression.TemplateSpans.Nodes {
541
+ span := spanNode.AsTemplateSpan()
542
+ if span == nil || span.Literal == nil {
543
+ return nil, false
544
+ }
545
+ literal := span.Literal
546
+ start := shimscanner.SkipTrivia(source, literal.Pos())
547
+ end := literal.End()
548
+ if start < 0 || end > len(source) || start+2 > end || source[start] != '}' {
549
+ return nil, false
550
+ }
551
+ suffix := 1
552
+ if literal.Kind == shimast.KindTemplateMiddle {
553
+ suffix = 2
554
+ if start+3 > end || source[end-2:end] != "${" {
555
+ return nil, false
556
+ }
557
+ } else if literal.Kind != shimast.KindTemplateTail || source[end-1] != '`' {
558
+ return nil, false
559
+ }
560
+ quasis = append(quasis, unicornTemplateIndentQuasi{
561
+ pos: start + 1,
562
+ end: end - suffix,
563
+ text: source[start+1 : end-suffix],
564
+ })
565
+ }
566
+ return quasis, true
567
+ }
568
+ return nil, false
569
+ }
570
+
571
+ func unicornTemplateIndentMarker(quasis []unicornTemplateIndentQuasi) string {
572
+ for index := 0; ; index++ {
573
+ marker := fmt.Sprintf("\x00TTSC_TEMPLATE_INDENT_%d\x00", index)
574
+ available := true
575
+ for _, quasi := range quasis {
576
+ if strings.Contains(quasi.text, marker) {
577
+ available = false
578
+ break
579
+ }
580
+ }
581
+ if available {
582
+ return marker
583
+ }
584
+ }
585
+ }
586
+
587
+ func unicornTemplateIndentFirstEOL(text string) string {
588
+ newline := strings.IndexByte(text, '\n')
589
+ if newline < 0 {
590
+ return ""
591
+ }
592
+ if newline > 0 && text[newline-1] == '\r' {
593
+ return "\r\n"
594
+ }
595
+ return "\n"
596
+ }
597
+
598
+ func unicornTemplateIndentParentMargin(source string, pos int) string {
599
+ if pos < 0 || pos > len(source) {
600
+ return ""
601
+ }
602
+ lineStart := 0
603
+ for index := 0; index < pos; {
604
+ if width := unicornTemplateIndentLineBreakWidth(source, index); width > 0 {
605
+ lineStart = index + width
606
+ index += width
607
+ continue
608
+ }
609
+ _, width := utf8.DecodeRuneInString(source[index:])
610
+ index += width
611
+ }
612
+ lineEnd := len(source)
613
+ for index := pos; index < len(source); {
614
+ if width := unicornTemplateIndentLineBreakWidth(source, index); width > 0 {
615
+ lineEnd = index
616
+ break
617
+ }
618
+ _, width := utf8.DecodeRuneInString(source[index:])
619
+ index += width
620
+ }
621
+ line := source[lineStart:lineEnd]
622
+ index := 0
623
+ if lineStart == 0 && strings.HasPrefix(line, "\uFEFF") {
624
+ index = len("\uFEFF")
625
+ }
626
+ marginStart := index
627
+ for index < len(line) {
628
+ character, size := utf8.DecodeRuneInString(line[index:])
629
+ if !unicornTemplateIndentIsECMAScriptWhitespace(character) {
630
+ break
631
+ }
632
+ index += size
633
+ }
634
+ if index == len(line) {
635
+ return ""
636
+ }
637
+ return line[marginStart:index]
638
+ }
639
+
640
+ func unicornTemplateIndentStrip(text string) string {
641
+ lines := unicornTemplateIndentSplitLines(text)
642
+ minimum := -1
643
+ for _, line := range lines {
644
+ indent := 0
645
+ for indent < len(line.text) && (line.text[indent] == ' ' || line.text[indent] == '\t') {
646
+ indent++
647
+ }
648
+ if unicornTemplateIndentWhitespaceOnly(line.text[indent:]) {
649
+ continue
650
+ }
651
+ if minimum < 0 || indent < minimum {
652
+ minimum = indent
653
+ }
654
+ }
655
+ if minimum <= 0 {
656
+ return text
657
+ }
658
+ for index := range lines {
659
+ removable := 0
660
+ for removable < len(lines[index].text) && removable < minimum &&
661
+ (lines[index].text[removable] == ' ' || lines[index].text[removable] == '\t') {
662
+ removable++
663
+ }
664
+ if removable == minimum {
665
+ lines[index].text = lines[index].text[minimum:]
666
+ }
667
+ }
668
+ return unicornTemplateIndentJoinLines(lines)
669
+ }
670
+
671
+ func unicornTemplateIndentIndentNonEmpty(text, indent string) string {
672
+ lines := unicornTemplateIndentSplitLines(text)
673
+ for index := range lines {
674
+ if !unicornTemplateIndentWhitespaceOnly(lines[index].text) {
675
+ lines[index].text = indent + lines[index].text
676
+ }
677
+ }
678
+ return unicornTemplateIndentJoinLines(lines)
679
+ }
680
+
681
+ func unicornTemplateIndentSplitLines(text string) []unicornTemplateIndentLine {
682
+ lines := make([]unicornTemplateIndentLine, 0, strings.Count(text, "\n")+1)
683
+ start := 0
684
+ for index := 0; index < len(text); {
685
+ if width := unicornTemplateIndentLineBreakWidth(text, index); width > 0 {
686
+ lines = append(lines, unicornTemplateIndentLine{
687
+ text: text[start:index],
688
+ terminator: text[index : index+width],
689
+ })
690
+ index += width
691
+ start = index
692
+ continue
693
+ }
694
+ _, width := utf8.DecodeRuneInString(text[index:])
695
+ index += width
696
+ }
697
+ return append(lines, unicornTemplateIndentLine{text: text[start:]})
698
+ }
699
+
700
+ func unicornTemplateIndentJoinLines(lines []unicornTemplateIndentLine) string {
701
+ var joined strings.Builder
702
+ for _, line := range lines {
703
+ joined.WriteString(line.text)
704
+ joined.WriteString(line.terminator)
705
+ }
706
+ return joined.String()
707
+ }
708
+
709
+ func unicornTemplateIndentOnlyHorizontalSpace(text string) bool {
710
+ for index := 0; index < len(text); index++ {
711
+ if text[index] != ' ' && text[index] != '\t' {
712
+ return false
713
+ }
714
+ }
715
+ return true
716
+ }
717
+
718
+ func unicornTemplateIndentIgnoredLines(source string, templates []*shimast.Node) map[int]struct{} {
719
+ starts := unicornTemplateIndentSourceLineStarts(source)
720
+ ignored := make(map[int]struct{})
721
+ for _, template := range templates {
722
+ start := shimscanner.SkipTrivia(source, template.Pos())
723
+ end := template.End()
724
+ if start < 0 || start >= len(source) || end <= start {
725
+ continue
726
+ }
727
+ if end > len(source) {
728
+ end = len(source)
729
+ }
730
+ startLine := unicornTemplateIndentLineAt(starts, start)
731
+ endLine := unicornTemplateIndentLineAt(starts, end-1)
732
+ for line := startLine + 1; line <= endLine; line++ {
733
+ ignored[line] = struct{}{}
734
+ }
735
+ }
736
+ return ignored
737
+ }
738
+
739
+ func unicornTemplateIndentSourceLineStarts(source string) []int {
740
+ starts := []int{0}
741
+ for index := 0; index < len(source); {
742
+ if width := unicornTemplateIndentLineBreakWidth(source, index); width > 0 {
743
+ index += width
744
+ starts = append(starts, index)
745
+ continue
746
+ }
747
+ _, width := utf8.DecodeRuneInString(source[index:])
748
+ index += width
749
+ }
750
+ return starts
751
+ }
752
+
753
+ func unicornTemplateIndentLineBreakWidth(source string, index int) int {
754
+ if index < 0 || index >= len(source) {
755
+ return 0
756
+ }
757
+ switch source[index] {
758
+ case '\r':
759
+ if index+1 < len(source) && source[index+1] == '\n' {
760
+ return 2
761
+ }
762
+ return 1
763
+ case '\n':
764
+ return 1
765
+ }
766
+ character, width := utf8.DecodeRuneInString(source[index:])
767
+ if character == '\u2028' || character == '\u2029' {
768
+ return width
769
+ }
770
+ return 0
771
+ }
772
+
773
+ func unicornTemplateIndentLineAt(starts []int, pos int) int {
774
+ index := sort.Search(len(starts), func(index int) bool { return starts[index] > pos })
775
+ if index == 0 {
776
+ return 0
777
+ }
778
+ return index - 1
779
+ }
780
+
781
+ func unicornTemplateIndentSourceForDetection(source string, ignored map[int]struct{}) string {
782
+ var normalized strings.Builder
783
+ normalized.Grow(len(source))
784
+ index := 0
785
+ if strings.HasPrefix(source, "\uFEFF") {
786
+ index = len("\uFEFF")
787
+ }
788
+ for index < len(source) {
789
+ if width := unicornTemplateIndentLineBreakWidth(source, index); width > 0 {
790
+ normalized.WriteByte('\n')
791
+ index += width
792
+ continue
793
+ }
794
+ _, width := utf8.DecodeRuneInString(source[index:])
795
+ normalized.WriteString(source[index : index+width])
796
+ index += width
797
+ }
798
+ return unicornTemplateIndentLinesForDetection(normalized.String(), ignored)
799
+ }
800
+
801
+ func unicornTemplateIndentTextForDetection(text string) string {
802
+ return unicornTemplateIndentLinesForDetection(strings.ReplaceAll(text, "\r\n", "\n"), nil)
803
+ }
804
+
805
+ func unicornTemplateIndentLinesForDetection(text string, ignored map[int]struct{}) string {
806
+ lines := strings.Split(text, "\n")
807
+ for index, line := range lines {
808
+ if _, skip := ignored[index]; skip || unicornTemplateIndentWhitespaceOnly(line) {
809
+ lines[index] = ""
810
+ }
811
+ }
812
+ return strings.Join(lines, "\n")
813
+ }
814
+
815
+ func unicornTemplateIndentWhitespaceOnly(text string) bool {
816
+ for _, character := range text {
817
+ if !unicornTemplateIndentIsECMAScriptWhitespace(character) {
818
+ return false
819
+ }
820
+ }
821
+ return true
822
+ }
823
+
824
+ func unicornTemplateIndentTrim(text string) string {
825
+ return strings.TrimFunc(text, unicornTemplateIndentIsECMAScriptWhitespace)
826
+ }
827
+
828
+ func unicornTemplateIndentIsECMAScriptWhitespace(character rune) bool {
829
+ if unicode.In(character, unicode.Zs) {
830
+ return true
831
+ }
832
+ switch character {
833
+ case '\t', '\v', '\f', '\n', '\r', '\u2028', '\u2029', '\uFEFF':
834
+ return true
835
+ default:
836
+ return false
837
+ }
838
+ }
839
+
840
+ func unicornTemplateIndentDetect(text string) string {
841
+ stats := unicornTemplateIndentStats(text, true)
842
+ if len(stats) == 0 {
843
+ stats = unicornTemplateIndentStats(text, false)
844
+ }
845
+ if len(stats) == 0 {
846
+ return ""
847
+ }
848
+ best := stats[0]
849
+ for _, stat := range stats[1:] {
850
+ if stat.uses > best.uses || stat.uses == best.uses && stat.weight > best.weight {
851
+ best = stat
852
+ }
853
+ }
854
+ character := " "
855
+ if best.kind == 't' {
856
+ character = "\t"
857
+ }
858
+ return strings.Repeat(character, best.amount)
859
+ }
860
+
861
+ func unicornTemplateIndentStats(text string, ignoreSingleSpaces bool) []unicornTemplateIndentStat {
862
+ stats := make([]unicornTemplateIndentStat, 0)
863
+ indexes := make(map[[2]int]int)
864
+ previousSize := 0
865
+ var previousKind byte
866
+ current := -1
867
+
868
+ for _, line := range strings.Split(text, "\n") {
869
+ if line == "" {
870
+ continue
871
+ }
872
+ kind, size := unicornTemplateIndentLeadingRun(line)
873
+ if size == 0 {
874
+ previousSize = 0
875
+ previousKind = 0
876
+ current = -1
877
+ continue
878
+ }
879
+ if ignoreSingleSpaces && kind == 's' && size == 1 {
880
+ continue
881
+ }
882
+ if kind != previousKind {
883
+ previousSize = 0
884
+ }
885
+ previousKind = kind
886
+ difference := size - previousSize
887
+ previousSize = size
888
+ if difference == 0 {
889
+ if current >= 0 {
890
+ stats[current].weight++
891
+ }
892
+ continue
893
+ }
894
+ if difference < 0 {
895
+ difference = -difference
896
+ }
897
+ if ignoreSingleSpaces && kind == 's' && difference == 1 {
898
+ continue
899
+ }
900
+ key := [2]int{int(kind), difference}
901
+ if index, exists := indexes[key]; exists {
902
+ current = index
903
+ stats[index].uses++
904
+ continue
905
+ }
906
+ current = len(stats)
907
+ indexes[key] = current
908
+ stats = append(stats, unicornTemplateIndentStat{kind: kind, amount: difference, uses: 1})
909
+ }
910
+ return stats
911
+ }
912
+
913
+ func unicornTemplateIndentLeadingRun(line string) (byte, int) {
914
+ if line == "" {
915
+ return 0, 0
916
+ }
917
+ character := line[0]
918
+ if character != ' ' && character != '\t' {
919
+ return 0, 0
920
+ }
921
+ end := 1
922
+ for end < len(line) && line[end] == character {
923
+ end++
924
+ }
925
+ if character == '\t' {
926
+ return 't', end
927
+ }
928
+ return 's', end
24
929
  }
25
930
 
26
931
  func init() {