@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
@@ -1,27 +1,947 @@
1
- // unicorn/isolated-functions: flags function expressions that "escape"
2
- // their lexical closure unintentionally e.g. an inner function that
3
- // references no outer binding but is still placed at the inner scope.
4
- // Correctly identifying "isolated function" context requires scope
5
- // analysis tracking what each nested function captures, which is not
6
- // available in the AST-only slice of this port.
1
+ // unicornIsolatedFunctions ports unicorn/isolated-functions: functions that
2
+ // run outside their defining execution context (workerized callbacks,
3
+ // `page.evaluate` payloads, `@isolated`-tagged functions, selector-matched
4
+ // functions) must not reference bindings from their surrounding scope, `this`,
5
+ // or `super`.
7
6
  //
8
- // Registered as a no-op stub to keep
9
- // `TestTypedKeysMatchRegisteredRules` aligned with the typed
10
- // `ITtscLintUnicornRules` surface. The matching fixture under
11
- // `tests/test-lint/src/cases/unicorn-isolated-functions.ts` keeps its
12
- // `@ttsc-corpus-skip` directive.
7
+ // Scope escape is decided by the TypeScript-Go checker: a reference inside an
8
+ // isolated function is allowed only when its symbol has a declaration inside
9
+ // the function (parameters, locals, nested declarations). ESLint's
10
+ // language-options globals have no direct equivalent here, so the port maps
11
+ // "configured globals" to ambient global declarations (lib files, `@types`
12
+ // packages, `declare global` blocks): those stay allowed read-only, mirroring
13
+ // upstream's all-readonly ES-globals default, and the `overrideGlobals` option
14
+ // remains the writability/off escape hatch. Everything else — module and
15
+ // script bindings, imports, recursion through the function's own hoisted name,
16
+ // unresolved names — is reported exactly like upstream's `scope.through`
17
+ // analysis.
13
18
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/isolated-functions.md
14
19
  package linthost
15
20
 
16
- import shimast "github.com/microsoft/typescript-go/shim/ast"
21
+ import (
22
+ "bytes"
23
+ "encoding/json"
24
+ "fmt"
25
+ "regexp"
26
+ "sort"
27
+ "strconv"
28
+ "strings"
17
29
 
18
- type unicornIsolatedFunctions struct{}
30
+ shimast "github.com/microsoft/typescript-go/shim/ast"
31
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
32
+ )
33
+
34
+ var (
35
+ unicornIsolatedFunctionsDefaultFunctions = []string{"makeSynchronous", "workerize"}
36
+ unicornIsolatedFunctionsDefaultComments = []string{"@isolated"}
37
+ // JSDoc block comments keep their `*` margin in the raw comment interior;
38
+ // upstream strips one leading run of `*`-and-whitespace before matching.
39
+ unicornIsolatedFunctionsCommentMarginPattern = regexp.MustCompile(`^(?:\*\s*)*`)
40
+ )
41
+
42
+ type unicornIsolatedFunctions struct{ optionsRule }
43
+
44
+ type unicornIsolatedFunctionsRawOptions struct {
45
+ Functions json.RawMessage `json:"functions"`
46
+ Selectors json.RawMessage `json:"selectors"`
47
+ Comments json.RawMessage `json:"comments"`
48
+ OverrideGlobals json.RawMessage `json:"overrideGlobals"`
49
+ }
50
+
51
+ // unicornIsolatedFunctionsGlobalPolicy is one normalized overrideGlobals
52
+ // value. Upstream accepts booleans plus the writability strings; `false` and
53
+ // "readonly" both mean readable-but-not-writable, so the port collapses them.
54
+ type unicornIsolatedFunctionsGlobalPolicy uint8
55
+
56
+ const (
57
+ unicornIsolatedFunctionsGlobalReadonly unicornIsolatedFunctionsGlobalPolicy = iota
58
+ unicornIsolatedFunctionsGlobalWritable
59
+ unicornIsolatedFunctionsGlobalOff
60
+ )
61
+
62
+ type unicornIsolatedFunctionsSelector struct {
63
+ source string
64
+ selector *astSelector
65
+ }
66
+
67
+ type unicornIsolatedFunctionsOptions struct {
68
+ functions []string
69
+ selectors []unicornIsolatedFunctionsSelector
70
+ comments []string
71
+ overrideGlobals map[string]unicornIsolatedFunctionsGlobalPolicy
72
+ }
73
+
74
+ type unicornIsolatedFunctionsProblem struct {
75
+ pos int
76
+ node *shimast.Node
77
+ message string
78
+ }
79
+
80
+ type unicornIsolatedFunctionsAnalysis struct {
81
+ ctx *Context
82
+ options unicornIsolatedFunctionsOptions
83
+ factory *shimast.NodeFactory
84
+ selected []map[*shimast.Node]struct{}
85
+ problems []unicornIsolatedFunctionsProblem
86
+ walkCB func(*shimast.Node) bool
87
+ }
19
88
 
20
89
  func (unicornIsolatedFunctions) Name() string { return "unicorn/isolated-functions" }
90
+
91
+ // NeedsTypeChecker: scope escape is decided with checker symbol resolution so
92
+ // shadowing, destructuring, hoisting, and TypeScript declarations keep their
93
+ // real binding identity instead of a name-only approximation.
94
+ func (unicornIsolatedFunctions) NeedsTypeChecker() bool { return true }
95
+
21
96
  func (unicornIsolatedFunctions) Visits() []shimast.Kind {
22
97
  return []shimast.Kind{shimast.KindSourceFile}
23
98
  }
24
- func (unicornIsolatedFunctions) Check(_ *Context, _ *shimast.Node) {
99
+
100
+ // ValidateOptions is consumed by the engine's optional rule-options
101
+ // validation interface. Parsing here makes malformed selectors and
102
+ // overrideGlobals values a project configuration error before any file is
103
+ // linted.
104
+ func (unicornIsolatedFunctions) ValidateOptions(raw json.RawMessage) error {
105
+ _, err := compileUnicornIsolatedFunctionsOptions(raw)
106
+ return err
107
+ }
108
+
109
+ func (unicornIsolatedFunctions) Check(ctx *Context, root *shimast.Node) {
110
+ if ctx == nil || ctx.File == nil || ctx.Checker == nil || root == nil {
111
+ return
112
+ }
113
+ options, err := compileUnicornIsolatedFunctionsOptions(ctx.Options)
114
+ if err != nil {
115
+ // Engine construction already records this as a configuration error.
116
+ // Check stays side-effect-free for direct contributor calls.
117
+ return
118
+ }
119
+
120
+ analysis := &unicornIsolatedFunctionsAnalysis{
121
+ ctx: ctx,
122
+ options: options,
123
+ factory: shimast.NewNodeFactory(shimast.NodeFactoryHooks{}),
124
+ }
125
+ analysis.selected = make([]map[*shimast.Node]struct{}, len(options.selectors))
126
+ for index, entry := range options.selectors {
127
+ matched := matchASTSelector(root, entry.selector)
128
+ set := make(map[*shimast.Node]struct{}, len(matched))
129
+ for _, node := range matched {
130
+ set[node] = struct{}{}
131
+ }
132
+ analysis.selected[index] = set
133
+ }
134
+
135
+ analysis.walkCB = analysis.visit
136
+ analysis.walk(root)
137
+
138
+ // ESLint sorts the final diagnostics by source position. The stable sort
139
+ // preserves the upstream inner-before-outer report order when nested
140
+ // isolated functions flag the same identifier.
141
+ sort.SliceStable(analysis.problems, func(left, right int) bool {
142
+ return analysis.problems[left].pos < analysis.problems[right].pos
143
+ })
144
+ for _, problem := range analysis.problems {
145
+ ctx.Report(problem.node, problem.message)
146
+ }
147
+ }
148
+
149
+ // walk processes functions on exit (children first), matching upstream's
150
+ // `context.onExit` registration: nested isolated functions report before the
151
+ // enclosing one.
152
+ func (a *unicornIsolatedFunctionsAnalysis) walk(node *shimast.Node) {
153
+ if node == nil {
154
+ return
155
+ }
156
+ node.ForEachChild(a.walkCB)
157
+ if unicornIsolatedFunctionsIsFunction(node) {
158
+ if reason := a.isolationReason(node); reason != "" {
159
+ a.analyzeFunction(node, reason)
160
+ }
161
+ }
162
+ }
163
+
164
+ func (a *unicornIsolatedFunctionsAnalysis) visit(child *shimast.Node) bool {
165
+ a.walk(child)
166
+ return false
167
+ }
168
+
169
+ // unicornIsolatedFunctionsIsFunction lists the function shapes a reason can
170
+ // attach to. Upstream's ESTree function types are declaration, expression, and
171
+ // arrow; TypeScript-Go represents class/object methods, accessors, and
172
+ // constructors as their own function nodes (ESTree wraps a FunctionExpression
173
+ // value), so those participate directly.
174
+ func unicornIsolatedFunctionsIsFunction(node *shimast.Node) bool {
175
+ if node == nil {
176
+ return false
177
+ }
178
+ switch node.Kind {
179
+ case shimast.KindFunctionDeclaration,
180
+ shimast.KindFunctionExpression,
181
+ shimast.KindArrowFunction,
182
+ shimast.KindMethodDeclaration,
183
+ shimast.KindGetAccessor,
184
+ shimast.KindSetAccessor,
185
+ shimast.KindConstructor:
186
+ return true
187
+ }
188
+ return false
189
+ }
190
+
191
+ // isolationReason mirrors upstream's reasonForBeingIsolatedFunction plus its
192
+ // selector listeners: comment markers win, then the `functions` option, then
193
+ // the built-in browser.execute/page.evaluate/executeScript shapes, then the
194
+ // configured selectors in option order. The first reason claims the function;
195
+ // upstream's `checked` set never lets a second listener re-report it.
196
+ func (a *unicornIsolatedFunctionsAnalysis) isolationReason(node *shimast.Node) string {
197
+ if len(a.options.comments) > 0 {
198
+ if interior, ok := a.findIsolationComment(node); ok {
199
+ value := unicornIsolatedFunctionsCommentMarginPattern.ReplaceAllString(interior, "")
200
+ value = strings.ToLower(strings.TrimSpace(value))
201
+ for _, comment := range a.options.comments {
202
+ if value == comment ||
203
+ strings.HasPrefix(value, comment+" - ") ||
204
+ strings.HasPrefix(value, comment+" -- ") {
205
+ return "follows comment " + unicornIsolatedFunctionsQuote(comment)
206
+ }
207
+ }
208
+ }
209
+ }
210
+ if reason := a.callArgumentReason(node); reason != "" {
211
+ return reason
212
+ }
213
+ for index, entry := range a.options.selectors {
214
+ if _, ok := a.selected[index][node]; ok {
215
+ return "matches selector " + unicornIsolatedFunctionsQuote(entry.source)
216
+ }
217
+ }
218
+ return ""
219
+ }
220
+
221
+ // callArgumentReason covers the call-shaped reasons: any argument of a
222
+ // configured function name, the first argument of `browser.execute` /
223
+ // `page.evaluate`, and the `func` property of an object passed first to
224
+ // `chrome.scripting.executeScript` / `browser.scripting.executeScript`.
225
+ // Parentheses are transparent because ESTree drops them.
226
+ func (a *unicornIsolatedFunctionsAnalysis) callArgumentReason(node *shimast.Node) string {
227
+ parent := unicornIsolatedFunctionsParent(node)
228
+ if parent != nil && parent.Kind == shimast.KindCallExpression {
229
+ call := parent.AsCallExpression()
230
+ if call != nil && call.Arguments != nil {
231
+ isArgument := false
232
+ isFirstArgument := false
233
+ for index, argument := range call.Arguments.Nodes {
234
+ if stripParens(argument) == node {
235
+ isArgument = true
236
+ isFirstArgument = index == 0
237
+ break
238
+ }
239
+ }
240
+ if isArgument && len(a.options.functions) > 0 {
241
+ if name := identifierText(stripParens(call.Expression)); name != "" {
242
+ for _, function := range a.options.functions {
243
+ if function == name {
244
+ return "callee of function named " + unicornIsolatedFunctionsQuote(name)
245
+ }
246
+ }
247
+ }
248
+ }
249
+ if isFirstArgument {
250
+ switch unicornIsolatedFunctionsMethodObjectName(call, "execute") {
251
+ case "browser":
252
+ return `callee of method named "browser.execute"`
253
+ }
254
+ switch unicornIsolatedFunctionsMethodObjectName(call, "evaluate") {
255
+ case "page":
256
+ return `callee of method named "page.evaluate"`
257
+ }
258
+ }
259
+ }
260
+ }
261
+ return a.executeScriptFuncReason(node)
262
+ }
263
+
264
+ // unicornIsolatedFunctionsMethodObjectName returns the simple-identifier
265
+ // receiver of a non-computed `object.method(...)` call, or "" when the callee
266
+ // has any other shape. Optional chaining is accepted, mirroring upstream's
267
+ // unconstrained isMethodCall options.
268
+ func unicornIsolatedFunctionsMethodObjectName(call *shimast.CallExpression, method string) string {
269
+ if call == nil {
270
+ return ""
271
+ }
272
+ callee := stripParens(call.Expression)
273
+ if callee == nil || callee.Kind != shimast.KindPropertyAccessExpression {
274
+ return ""
275
+ }
276
+ access := callee.AsPropertyAccessExpression()
277
+ if access == nil || identifierText(access.Name()) != method {
278
+ return ""
279
+ }
280
+ return identifierText(stripParens(access.Expression))
281
+ }
282
+
283
+ // executeScriptFuncReason recognizes `func`-valued properties (assignment,
284
+ // string key, computed string key, or method shorthand — upstream Property
285
+ // kind "init") on an object literal passed as the first argument of
286
+ // `<browser|chrome>.scripting.executeScript(...)`. Accessors stay excluded.
287
+ func (a *unicornIsolatedFunctionsAnalysis) executeScriptFuncReason(node *shimast.Node) string {
288
+ var property *shimast.Node
289
+ switch node.Kind {
290
+ case shimast.KindMethodDeclaration:
291
+ property = node
292
+ case shimast.KindFunctionExpression, shimast.KindArrowFunction:
293
+ parent := unicornIsolatedFunctionsParent(node)
294
+ if parent == nil || parent.Kind != shimast.KindPropertyAssignment {
295
+ return ""
296
+ }
297
+ assignment := parent.AsPropertyAssignment()
298
+ if assignment == nil || stripParens(assignment.Initializer) != node {
299
+ return ""
300
+ }
301
+ property = parent
302
+ default:
303
+ return ""
304
+ }
305
+ if unicornIsolatedFunctionsObjectPropertyName(property.Name()) != "func" {
306
+ return ""
307
+ }
308
+ object := property.Parent
309
+ if object == nil || object.Kind != shimast.KindObjectLiteralExpression {
310
+ return ""
311
+ }
312
+ callNode := unicornIsolatedFunctionsParent(object)
313
+ if callNode == nil || callNode.Kind != shimast.KindCallExpression {
314
+ return ""
315
+ }
316
+ call := callNode.AsCallExpression()
317
+ if call == nil || call.Arguments == nil || len(call.Arguments.Nodes) == 0 ||
318
+ stripParens(call.Arguments.Nodes[0]) != object {
319
+ return ""
320
+ }
321
+ scripting := unicornIsolatedFunctionsMethodObjectAccess(call, "executeScript")
322
+ if scripting == nil || scripting.Kind != shimast.KindPropertyAccessExpression {
323
+ return ""
324
+ }
325
+ access := scripting.AsPropertyAccessExpression()
326
+ if access == nil || identifierText(access.Name()) != "scripting" {
327
+ return ""
328
+ }
329
+ base := identifierText(stripParens(access.Expression))
330
+ if base != "browser" && base != "chrome" {
331
+ return ""
332
+ }
333
+ return `property "func" passed to "` + base + `.scripting.executeScript"`
334
+ }
335
+
336
+ // unicornIsolatedFunctionsMethodObjectAccess returns the receiver expression
337
+ // of a non-computed `receiver.method(...)` call without constraining the
338
+ // receiver's shape.
339
+ func unicornIsolatedFunctionsMethodObjectAccess(call *shimast.CallExpression, method string) *shimast.Node {
340
+ if call == nil {
341
+ return nil
342
+ }
343
+ callee := stripParens(call.Expression)
344
+ if callee == nil || callee.Kind != shimast.KindPropertyAccessExpression {
345
+ return nil
346
+ }
347
+ access := callee.AsPropertyAccessExpression()
348
+ if access == nil || identifierText(access.Name()) != method {
349
+ return nil
350
+ }
351
+ return stripParens(access.Expression)
352
+ }
353
+
354
+ // unicornIsolatedFunctionsObjectPropertyName mirrors upstream's
355
+ // getObjectPropertyName: identifier keys and string-literal keys count, and a
356
+ // computed key counts only when it is a string literal.
357
+ func unicornIsolatedFunctionsObjectPropertyName(name *shimast.Node) string {
358
+ if name == nil {
359
+ return ""
360
+ }
361
+ switch name.Kind {
362
+ case shimast.KindIdentifier:
363
+ return identifierText(name)
364
+ case shimast.KindStringLiteral:
365
+ return stringLiteralText(name)
366
+ case shimast.KindComputedPropertyName:
367
+ computed := name.AsComputedPropertyName()
368
+ if computed == nil {
369
+ return ""
370
+ }
371
+ expression := stripParens(computed.Expression)
372
+ if expression != nil && expression.Kind == shimast.KindStringLiteral {
373
+ return stringLiteralText(expression)
374
+ }
375
+ }
376
+ return ""
377
+ }
378
+
379
+ // unicornIsolatedFunctionsParent returns the ESTree-equivalent parent:
380
+ // TypeScript-Go keeps ParenthesizedExpression nodes that ESLint's tree drops.
381
+ func unicornIsolatedFunctionsParent(node *shimast.Node) *shimast.Node {
382
+ if node == nil {
383
+ return nil
384
+ }
385
+ parent := node.Parent
386
+ for parent != nil && parent.Kind == shimast.KindParenthesizedExpression {
387
+ parent = parent.Parent
388
+ }
389
+ return parent
390
+ }
391
+
392
+ // findIsolationComment mirrors upstream's findComment: take the comment
393
+ // directly preceding the function, walking up through variable, export, and
394
+ // property declarations while no comment has been found yet. The first
395
+ // comment encountered ends the walk whether or not it matches.
396
+ func (a *unicornIsolatedFunctionsAnalysis) findIsolationComment(node *shimast.Node) (string, bool) {
397
+ commentable := node
398
+ for {
399
+ if interior, ok := a.lastLeadingComment(commentable); ok {
400
+ return interior, true
401
+ }
402
+ next := unicornIsolatedFunctionsCommentParent(commentable)
403
+ if next == nil {
404
+ return "", false
405
+ }
406
+ commentable = next
407
+ }
408
+ }
409
+
410
+ // unicornIsolatedFunctionsCommentParent maps upstream's
411
+ // canCommentApplyToParent chain onto the TypeScript-Go tree. ESTree's
412
+ // VariableDeclarator/VariableDeclaration/ExportNamedDeclaration hops become
413
+ // VariableDeclaration/VariableDeclarationList/VariableStatement (export
414
+ // modifiers live on the statement); ExportDefaultDeclaration is
415
+ // ExportAssignment; Property values hop to their PropertyAssignment. Class
416
+ // and object methods carry their own leading trivia, so they need no hop.
417
+ func unicornIsolatedFunctionsCommentParent(node *shimast.Node) *shimast.Node {
418
+ if node == nil {
419
+ return nil
420
+ }
421
+ wrapped := node
422
+ parent := node.Parent
423
+ for parent != nil && parent.Kind == shimast.KindParenthesizedExpression {
424
+ wrapped = parent
425
+ parent = parent.Parent
426
+ }
427
+ if parent == nil {
428
+ return nil
429
+ }
430
+ switch parent.Kind {
431
+ case shimast.KindVariableDeclaration,
432
+ shimast.KindVariableDeclarationList,
433
+ shimast.KindVariableStatement:
434
+ return parent
435
+ case shimast.KindExportAssignment:
436
+ assignment := parent.AsExportAssignment()
437
+ // `export = fn` is TSExportAssignment upstream, which is not in the
438
+ // comment-walk allowlist; only `export default` hops.
439
+ if assignment != nil && !assignment.IsExportEquals {
440
+ return parent
441
+ }
442
+ case shimast.KindPropertyAssignment:
443
+ assignment := parent.AsPropertyAssignment()
444
+ if assignment != nil && assignment.Initializer == wrapped {
445
+ return parent
446
+ }
447
+ }
448
+ return nil
449
+ }
450
+
451
+ // lastLeadingComment returns the interior text of the last comment inside the
452
+ // node's leading trivia. That comment is exactly what ESLint's
453
+ // getTokenBefore(node, {includeComments: true}) yields when the preceding
454
+ // token is a comment: no other token can sit between it and the node.
455
+ func (a *unicornIsolatedFunctionsAnalysis) lastLeadingComment(node *shimast.Node) (string, bool) {
456
+ if a.ctx.File == nil || node == nil {
457
+ return "", false
458
+ }
459
+ source := a.ctx.File.Text()
460
+ triviaStart := node.Pos()
461
+ if triviaStart < 0 || triviaStart > len(source) {
462
+ return "", false
463
+ }
464
+ tokenStart := shimscanner.SkipTrivia(source, triviaStart)
465
+ if tokenStart <= triviaStart || tokenStart > len(source) {
466
+ return "", false
467
+ }
468
+ var last shimast.CommentRange
469
+ found := false
470
+ consider := func(comment shimast.CommentRange) {
471
+ if comment.Pos() < triviaStart || comment.End() > tokenStart {
472
+ return
473
+ }
474
+ if !found || comment.End() > last.End() {
475
+ last = comment
476
+ found = true
477
+ }
478
+ }
479
+ for comment := range shimscanner.GetTrailingCommentRanges(a.factory, source, triviaStart) {
480
+ consider(comment)
481
+ }
482
+ for comment := range shimscanner.GetLeadingCommentRanges(a.factory, source, triviaStart) {
483
+ consider(comment)
484
+ }
485
+ if !found {
486
+ return "", false
487
+ }
488
+ pos, end := last.Pos(), last.End()
489
+ if pos+2 > end || end > len(source) {
490
+ return "", false
491
+ }
492
+ if last.Kind == shimast.KindMultiLineCommentTrivia {
493
+ if pos+4 > end {
494
+ return "", false
495
+ }
496
+ return source[pos+2 : end-2], true
497
+ }
498
+ return source[pos+2 : end], true
499
+ }
500
+
501
+ // analyzeFunction reports every scope-escaping reference and every `this` /
502
+ // `super` usage of one isolated function. The walk roots are the function's
503
+ // type parameters, parameters, return type, and body: a method's name
504
+ // (computed key) and decorators evaluate in the enclosing class scope, so
505
+ // they are not part of the isolated scope, mirroring ESTree where they belong
506
+ // to the MethodDefinition wrapper.
507
+ func (a *unicornIsolatedFunctionsAnalysis) analyzeFunction(function *shimast.Node, reason string) {
508
+ data := function.FunctionLikeData()
509
+ if data == nil {
510
+ return
511
+ }
512
+ roots := make([]*shimast.Node, 0, 8)
513
+ if data.TypeParameters != nil {
514
+ roots = append(roots, data.TypeParameters.Nodes...)
515
+ }
516
+ if data.Parameters != nil {
517
+ roots = append(roots, data.Parameters.Nodes...)
518
+ }
519
+ if data.Type != nil {
520
+ roots = append(roots, data.Type)
521
+ }
522
+ if body := function.Body(); body != nil {
523
+ roots = append(roots, body)
524
+ }
525
+
526
+ declarations := make(map[*shimast.Node]struct{})
527
+ writes := make(map[*shimast.Node]struct{})
528
+ for _, root := range roots {
529
+ walkDescendants(root, func(node *shimast.Node) {
530
+ if node.Kind != shimast.KindShorthandPropertyAssignment {
531
+ for _, identifier := range noLoopFuncNamedIdentifiers(node.Name()) {
532
+ declarations[identifier] = struct{}{}
533
+ }
534
+ }
535
+ for _, identifier := range writeTargetIdentifiers(node) {
536
+ writes[identifier] = struct{}{}
537
+ }
538
+ })
539
+ }
540
+ for _, root := range roots {
541
+ walkDescendants(root, func(node *shimast.Node) {
542
+ if node.Kind != shimast.KindIdentifier {
543
+ return
544
+ }
545
+ if _, declaration := declarations[node]; declaration {
546
+ return
547
+ }
548
+ if !unicornIsolatedFunctionsIsReference(node) {
549
+ return
550
+ }
551
+ a.checkReference(function, node, reason, writes)
552
+ })
553
+ }
554
+ for _, root := range roots {
555
+ a.contextWalk(root, reason)
556
+ }
557
+ }
558
+
559
+ // unicornIsolatedFunctionsIsReference filters identifiers down to runtime (or
560
+ // upstream-visible type) references. Identifiers directly inside a type
561
+ // reference or type query mirror upstream's TSTypeReference/TSTypeQuery skip;
562
+ // qualified names below them intentionally stay visible, matching upstream.
563
+ func unicornIsolatedFunctionsIsReference(node *shimast.Node) bool {
564
+ if !isValueReferenceIdentifier(node) {
565
+ return false
566
+ }
567
+ // TypeScript-Go models the `this` at the head of a type-query entity name
568
+ // (`typeof this.foo`) as an identifier named "this"; ESTree models every
569
+ // `this` as a ThisExpression, which is never a scope reference. `this` is
570
+ // reserved and can name no binding, so any identifier spelled "this" is that
571
+ // pseudo-identifier, owned by the `this`/`super` context walk. Leaving it
572
+ // here would double-report it as an externally-scoped variable.
573
+ if identifierText(node) == "this" {
574
+ return false
575
+ }
576
+ parent := node.Parent
577
+ if parent == nil {
578
+ return true
579
+ }
580
+ switch parent.Kind {
581
+ case shimast.KindMetaProperty:
582
+ // `import.meta` / `new.target` carry no scope reference.
583
+ return false
584
+ case shimast.KindTypeReference, shimast.KindTypeQuery:
585
+ return false
586
+ case shimast.KindBindingElement:
587
+ binding := parent.AsBindingElement()
588
+ if binding != nil && binding.PropertyName == node {
589
+ return false
590
+ }
591
+ }
592
+ return true
593
+ }
594
+
595
+ // checkReference decides one reference the way upstream walks
596
+ // `scope.through` + getAllowedGlobalValue: bindings declared inside the
597
+ // function are fine; ambient globals are readable (writable only through
598
+ // overrideGlobals); captured outer bindings and unresolved names are
599
+ // reported. A function's own hoisted name lives in the enclosing scope, so
600
+ // recursion is reported, exactly like upstream.
601
+ func (a *unicornIsolatedFunctionsAnalysis) checkReference(
602
+ function *shimast.Node,
603
+ identifier *shimast.Node,
604
+ reason string,
605
+ writes map[*shimast.Node]struct{},
606
+ ) {
607
+ name := identifierText(identifier)
608
+ if name == "" {
609
+ return
610
+ }
611
+
612
+ var symbol *shimast.Symbol
613
+ capturedArguments := false
614
+ if name == "arguments" {
615
+ // The checker's `arguments` symbol has no declarations, so locate its
616
+ // owning non-arrow function syntactically: inside the isolated function
617
+ // it is a local; outside it is a captured binding that overrideGlobals
618
+ // must not whitelist; without any owner it is an unresolved name.
619
+ for ancestor := identifier.Parent; ancestor != nil; ancestor = ancestor.Parent {
620
+ if !isFunctionLikeKind(ancestor) || ancestor.Kind == shimast.KindArrowFunction {
621
+ continue
622
+ }
623
+ if ancestor.Pos() >= function.Pos() && ancestor.End() <= function.End() {
624
+ return
625
+ }
626
+ capturedArguments = true
627
+ break
628
+ }
629
+ } else {
630
+ symbol = canonicalValueSymbol(a.ctx, identifier)
631
+ if symbol != nil {
632
+ for _, declaration := range symbol.Declarations {
633
+ if declaration != nil && declaration != function &&
634
+ declaration.Pos() >= function.Pos() && declaration.End() <= function.End() {
635
+ return
636
+ }
637
+ }
638
+ }
639
+ }
640
+
641
+ global := symbol != nil && unicornIsolatedFunctionsIsAmbientGlobal(a.ctx, name, symbol)
642
+ captured := capturedArguments || (symbol != nil && !global)
643
+
644
+ problemReason := reason
645
+ policy, hasOverride := a.options.overrideGlobals[name]
646
+ allowed := false
647
+ if hasOverride {
648
+ if policy != unicornIsolatedFunctionsGlobalOff && !captured {
649
+ allowed = true
650
+ }
651
+ } else if global {
652
+ policy = unicornIsolatedFunctionsGlobalReadonly
653
+ allowed = true
654
+ }
655
+ if allowed {
656
+ if _, written := writes[identifier]; !written {
657
+ return
658
+ }
659
+ if policy == unicornIsolatedFunctionsGlobalWritable {
660
+ return
661
+ }
662
+ problemReason += " (global variable is not writable)"
663
+ }
664
+
665
+ a.addProblem(identifier,
666
+ "Variable "+name+" not defined in scope of isolated function. Function is isolated because: "+problemReason+".")
667
+ }
668
+
669
+ // unicornIsolatedFunctionsIsAmbientGlobal reports whether the symbol is this
670
+ // engine's equivalent of an ESLint configured global: every declaration is
671
+ // ambient (lib/@types declaration files or `declare` contexts) and the name
672
+ // resolves in the checker's global scope. Intrinsics such as `undefined` and
673
+ // `globalThis` have no declarations and count as globals. Script-file
674
+ // top-level bindings resolve globally but are real source declarations, so
675
+ // they stay reported like upstream's script-mode captures.
676
+ func unicornIsolatedFunctionsIsAmbientGlobal(ctx *Context, name string, symbol *shimast.Symbol) bool {
677
+ if len(symbol.Declarations) == 0 {
678
+ return true
679
+ }
680
+ for _, declaration := range symbol.Declarations {
681
+ if declaration == nil {
682
+ return false
683
+ }
684
+ if declaration.Flags&shimast.NodeFlagsAmbient != 0 {
685
+ continue
686
+ }
687
+ file := shimast.GetSourceFileOfNode(declaration)
688
+ if file == nil || !file.IsDeclarationFile {
689
+ return false
690
+ }
691
+ }
692
+ meaning := shimast.SymbolFlagsValue | shimast.SymbolFlagsType |
693
+ shimast.SymbolFlagsNamespace | shimast.SymbolFlagsAlias
694
+ global := ctx.Checker.ResolveName(name, nil, meaning, false /*excludeGlobals*/)
695
+ if global == nil {
696
+ return false
697
+ }
698
+ return ctx.Checker.GetMergedSymbol(global) == symbol
699
+ }
700
+
701
+ // contextWalk reports `this` and `super` usage the way upstream's
702
+ // getFunctionContextProblems does: nested non-arrow functions own their
703
+ // context and stop the walk (their computed keys still evaluate outside, so
704
+ // those are walked); nested classes contribute only their `extends`
705
+ // expression and computed member keys. A `typeof this` type query is reported
706
+ // as `this` because the upstream TS tree models it as a ThisExpression.
707
+ func (a *unicornIsolatedFunctionsAnalysis) contextWalk(node *shimast.Node, reason string) {
708
+ if node == nil {
709
+ return
710
+ }
711
+ switch node.Kind {
712
+ case shimast.KindThisKeyword:
713
+ a.addProblem(node, "Unexpected `this` in isolated function. Function is isolated because: "+reason+".")
714
+ return
715
+ case shimast.KindSuperKeyword:
716
+ a.addProblem(node, "Unexpected `super` in isolated function. Function is isolated because: "+reason+".")
717
+ return
718
+ case shimast.KindClassDeclaration, shimast.KindClassExpression:
719
+ a.contextWalk(unicornIsolatedFunctionsExtendsExpression(node), reason)
720
+ for _, member := range unicornIsolatedFunctionsClassMembers(node) {
721
+ if name := member.Name(); name != nil && name.Kind == shimast.KindComputedPropertyName {
722
+ a.contextWalk(name, reason)
723
+ }
724
+ }
725
+ return
726
+ case shimast.KindTypeQuery:
727
+ query := node.AsTypeQueryNode()
728
+ if query != nil {
729
+ if thisName := unicornIsolatedFunctionsEntityHead(query.ExprName); thisName != nil &&
730
+ identifierText(thisName) == "this" {
731
+ a.addProblem(thisName, "Unexpected `this` in isolated function. Function is isolated because: "+reason+".")
732
+ }
733
+ }
734
+ }
735
+ if isFunctionLikeKind(node) && node.Kind != shimast.KindArrowFunction {
736
+ if name := node.Name(); name != nil && name.Kind == shimast.KindComputedPropertyName {
737
+ a.contextWalk(name, reason)
738
+ }
739
+ return
740
+ }
741
+ node.ForEachChild(func(child *shimast.Node) bool {
742
+ a.contextWalk(child, reason)
743
+ return false
744
+ })
745
+ }
746
+
747
+ // unicornIsolatedFunctionsExtendsExpression returns the runtime `extends`
748
+ // expression of a class, or nil. Type arguments and `implements` clauses are
749
+ // type-only and stay out of the context walk, mirroring upstream's superClass
750
+ // handling.
751
+ func unicornIsolatedFunctionsExtendsExpression(class *shimast.Node) *shimast.Node {
752
+ var clauses *shimast.NodeList
753
+ switch class.Kind {
754
+ case shimast.KindClassDeclaration:
755
+ declaration := class.AsClassDeclaration()
756
+ if declaration != nil {
757
+ clauses = declaration.HeritageClauses
758
+ }
759
+ case shimast.KindClassExpression:
760
+ expression := class.AsClassExpression()
761
+ if expression != nil {
762
+ clauses = expression.HeritageClauses
763
+ }
764
+ }
765
+ if clauses == nil {
766
+ return nil
767
+ }
768
+ for _, clause := range clauses.Nodes {
769
+ heritage := clause.AsHeritageClause()
770
+ if heritage == nil || heritage.Token != shimast.KindExtendsKeyword ||
771
+ heritage.Types == nil || len(heritage.Types.Nodes) == 0 {
772
+ continue
773
+ }
774
+ withArguments := heritage.Types.Nodes[0].AsExpressionWithTypeArguments()
775
+ if withArguments != nil {
776
+ return withArguments.Expression
777
+ }
778
+ }
779
+ return nil
780
+ }
781
+
782
+ func unicornIsolatedFunctionsClassMembers(class *shimast.Node) []*shimast.Node {
783
+ switch class.Kind {
784
+ case shimast.KindClassDeclaration:
785
+ declaration := class.AsClassDeclaration()
786
+ if declaration != nil && declaration.Members != nil {
787
+ return declaration.Members.Nodes
788
+ }
789
+ case shimast.KindClassExpression:
790
+ expression := class.AsClassExpression()
791
+ if expression != nil && expression.Members != nil {
792
+ return expression.Members.Nodes
793
+ }
794
+ }
795
+ return nil
796
+ }
797
+
798
+ // unicornIsolatedFunctionsEntityHead returns the leftmost identifier of a
799
+ // type-query entity name (`typeof a.b.c` → `a`).
800
+ func unicornIsolatedFunctionsEntityHead(entity *shimast.Node) *shimast.Node {
801
+ for entity != nil && entity.Kind == shimast.KindQualifiedName {
802
+ qualified := entity.AsQualifiedName()
803
+ if qualified == nil {
804
+ return nil
805
+ }
806
+ entity = qualified.Left
807
+ }
808
+ if entity != nil && entity.Kind == shimast.KindIdentifier {
809
+ return entity
810
+ }
811
+ return nil
812
+ }
813
+
814
+ func (a *unicornIsolatedFunctionsAnalysis) addProblem(node *shimast.Node, message string) {
815
+ pos := node.Pos()
816
+ if a.ctx.File != nil {
817
+ pos = shimscanner.SkipTrivia(a.ctx.File.Text(), pos)
818
+ }
819
+ a.problems = append(a.problems, unicornIsolatedFunctionsProblem{
820
+ pos: pos,
821
+ node: node,
822
+ message: message,
823
+ })
824
+ }
825
+
826
+ // unicornIsolatedFunctionsQuote is JSON.stringify for reason interpolation:
827
+ // like upstream, names, comments, and selectors appear JSON-quoted, and HTML
828
+ // characters (selector combinators like `>`) stay literal.
829
+ func unicornIsolatedFunctionsQuote(value string) string {
830
+ var buffer bytes.Buffer
831
+ encoder := json.NewEncoder(&buffer)
832
+ encoder.SetEscapeHTML(false)
833
+ if err := encoder.Encode(value); err != nil {
834
+ return strconv.Quote(value)
835
+ }
836
+ return strings.TrimSuffix(buffer.String(), "\n")
837
+ }
838
+
839
+ func compileUnicornIsolatedFunctionsOptions(raw json.RawMessage) (unicornIsolatedFunctionsOptions, error) {
840
+ options := unicornIsolatedFunctionsOptions{
841
+ functions: append([]string(nil), unicornIsolatedFunctionsDefaultFunctions...),
842
+ comments: append([]string(nil), unicornIsolatedFunctionsDefaultComments...),
843
+ }
844
+ raw = bytes.TrimSpace(raw)
845
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
846
+ return options, nil
847
+ }
848
+ if raw[0] != '{' {
849
+ return unicornIsolatedFunctionsOptions{}, fmt.Errorf("unicorn/isolated-functions options must be an object")
850
+ }
851
+ encoded := unicornIsolatedFunctionsRawOptions{}
852
+ if err := decodeStrictJSON(raw, &encoded); err != nil {
853
+ return unicornIsolatedFunctionsOptions{}, fmt.Errorf(
854
+ "unicorn/isolated-functions options must contain only functions, selectors, comments, and overrideGlobals: %w", err)
855
+ }
856
+
857
+ var err error
858
+ if options.functions, err = decodeUnicornIsolatedFunctionsStrings(
859
+ "functions", encoded.Functions, options.functions); err != nil {
860
+ return unicornIsolatedFunctionsOptions{}, err
861
+ }
862
+ if options.comments, err = decodeUnicornIsolatedFunctionsStrings(
863
+ "comments", encoded.Comments, options.comments); err != nil {
864
+ return unicornIsolatedFunctionsOptions{}, err
865
+ }
866
+ selectorSources, err := decodeUnicornIsolatedFunctionsStrings("selectors", encoded.Selectors, nil)
867
+ if err != nil {
868
+ return unicornIsolatedFunctionsOptions{}, err
869
+ }
870
+ options.selectors = make([]unicornIsolatedFunctionsSelector, 0, len(selectorSources))
871
+ for index, source := range selectorSources {
872
+ selector, err := parseASTSelector(source)
873
+ if err != nil {
874
+ return unicornIsolatedFunctionsOptions{}, fmt.Errorf(
875
+ "unicorn/isolated-functions selector %d %q is invalid: %w", index+1, source, err)
876
+ }
877
+ options.selectors = append(options.selectors, unicornIsolatedFunctionsSelector{
878
+ source: source,
879
+ selector: selector,
880
+ })
881
+ }
882
+ for index := range options.comments {
883
+ options.comments[index] = strings.ToLower(options.comments[index])
884
+ }
885
+
886
+ if trimmed := bytes.TrimSpace(encoded.OverrideGlobals); len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) {
887
+ if trimmed[0] != '{' {
888
+ return unicornIsolatedFunctionsOptions{}, fmt.Errorf(
889
+ "unicorn/isolated-functions overrideGlobals must be an object")
890
+ }
891
+ entries := map[string]json.RawMessage{}
892
+ if err := decodeStrictJSON(trimmed, &entries); err != nil {
893
+ return unicornIsolatedFunctionsOptions{}, fmt.Errorf(
894
+ "unicorn/isolated-functions overrideGlobals must map names to boolean, \"readonly\", \"writable\", \"writeable\", or \"off\": %w", err)
895
+ }
896
+ options.overrideGlobals = make(map[string]unicornIsolatedFunctionsGlobalPolicy, len(entries))
897
+ for name, value := range entries {
898
+ policy, err := decodeUnicornIsolatedFunctionsGlobalPolicy(name, value)
899
+ if err != nil {
900
+ return unicornIsolatedFunctionsOptions{}, err
901
+ }
902
+ options.overrideGlobals[name] = policy
903
+ }
904
+ }
905
+ return options, nil
906
+ }
907
+
908
+ func decodeUnicornIsolatedFunctionsStrings(name string, raw json.RawMessage, defaults []string) ([]string, error) {
909
+ raw = bytes.TrimSpace(raw)
910
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
911
+ return defaults, nil
912
+ }
913
+ if raw[0] != '[' {
914
+ return nil, fmt.Errorf("unicorn/isolated-functions %s must be an array of unique strings", name)
915
+ }
916
+ var values []string
917
+ if err := decodeStrictJSON(raw, &values); err != nil {
918
+ return nil, fmt.Errorf("unicorn/isolated-functions %s must be an array of unique strings: %w", name, err)
919
+ }
920
+ seen := make(map[string]struct{}, len(values))
921
+ for _, value := range values {
922
+ if _, duplicate := seen[value]; duplicate {
923
+ return nil, fmt.Errorf("unicorn/isolated-functions %s must not contain duplicate %q", name, value)
924
+ }
925
+ seen[value] = struct{}{}
926
+ }
927
+ return values, nil
928
+ }
929
+
930
+ func decodeUnicornIsolatedFunctionsGlobalPolicy(
931
+ name string,
932
+ raw json.RawMessage,
933
+ ) (unicornIsolatedFunctionsGlobalPolicy, error) {
934
+ value := string(bytes.TrimSpace(raw))
935
+ switch value {
936
+ case "true", `"writable"`, `"writeable"`:
937
+ return unicornIsolatedFunctionsGlobalWritable, nil
938
+ case "false", `"readonly"`:
939
+ return unicornIsolatedFunctionsGlobalReadonly, nil
940
+ case `"off"`:
941
+ return unicornIsolatedFunctionsGlobalOff, nil
942
+ }
943
+ return unicornIsolatedFunctionsGlobalReadonly, fmt.Errorf(
944
+ "unicorn/isolated-functions overrideGlobals[%q] must be a boolean, \"readonly\", \"writable\", \"writeable\", or \"off\"", name)
25
945
  }
26
946
 
27
947
  func init() {