@configg/oxc 0.1.5 → 0.1.6

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 (36) hide show
  1. package/README.md +5 -0
  2. package/package.json +4 -1
  3. package/src/anti-slop/LICENSE +21 -0
  4. package/src/anti-slop/UPSTREAM.md +8 -0
  5. package/src/anti-slop/index.ts +47 -0
  6. package/src/anti-slop/rules/no-array-filter-map.ts +28 -0
  7. package/src/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  8. package/src/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  9. package/src/anti-slop/rules/no-known-value-widening.ts +415 -0
  10. package/src/anti-slop/rules/no-module-mocking.ts +80 -0
  11. package/src/anti-slop/rules/no-object-parameters.ts +83 -0
  12. package/src/anti-slop/rules/no-reduce-accumulator-copy.ts +109 -0
  13. package/src/anti-slop/rules/no-reflect-apply.ts +28 -0
  14. package/src/anti-slop/rules/no-reflect-get.ts +28 -0
  15. package/src/anti-slop/rules/no-runtime-typeof.ts +77 -0
  16. package/src/anti-slop/rules/no-shape-in-symbol-names.ts +46 -0
  17. package/src/anti-slop/rules/no-unknown-parameters.ts +69 -0
  18. package/src/anti-slop/rules/no-unknown-returns.ts +82 -0
  19. package/src/anti-slop/rules/no-unknown-type-aliases.ts +54 -0
  20. package/src/anti-slop/rules/no-unsafe-dictionary-type.ts +154 -0
  21. package/src/anti-slop/rules/no-widen-then-assert.ts +366 -0
  22. package/src/anti-slop/rules/require-readable-spacing.ts +47 -0
  23. package/src/anti-slop/rules/require-safety-comment-for-type-assertion.ts +131 -0
  24. package/src/anti-slop/shared/array-method.ts +94 -0
  25. package/src/anti-slop/shared/dictionary-types.ts +515 -0
  26. package/src/anti-slop/shared/function-parameters.ts +49 -0
  27. package/src/anti-slop/shared/lexical-type-parameters.ts +61 -0
  28. package/src/anti-slop/shared/reflect-method.ts +24 -0
  29. package/src/anti-slop/shared/scope.ts +15 -0
  30. package/src/anti-slop/shared/type-alias-resolution.ts +250 -0
  31. package/src/anti-slop/vendor/eslint-stylistic/LICENSE +22 -0
  32. package/src/anti-slop/vendor/eslint-stylistic/UPSTREAM.md +28 -0
  33. package/src/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
  34. package/src/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
  35. package/src/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
  36. package/src/lint.ts +26 -2
@@ -0,0 +1,61 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+
3
+ type VisitorKeys = Readonly<Record<string, readonly string[]>>;
4
+
5
+ function isNode(value: unknown): value is ESTree.Node {
6
+ return (
7
+ typeof value === "object" &&
8
+ value !== null &&
9
+ "type" in value &&
10
+ typeof value.type === "string"
11
+ );
12
+ }
13
+
14
+ function collectInferTypeParameterNames(
15
+ node: ESTree.Node,
16
+ visitorKeys: VisitorKeys,
17
+ names: Set<string>,
18
+ ): void {
19
+ if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
20
+ const record = node as unknown as Readonly<Record<string, unknown>>;
21
+ for (const key of visitorKeys[node.type] ?? []) {
22
+ const value = record[key];
23
+ if (isNode(value)) {
24
+ collectInferTypeParameterNames(value, visitorKeys, names);
25
+ continue;
26
+ }
27
+ if (!Array.isArray(value)) continue;
28
+ for (const child of value) {
29
+ if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
30
+ }
31
+ }
32
+ }
33
+
34
+ /** Collect type binders that are in scope at a node and can shadow module aliases. */
35
+ export function lexicalTypeParameterNames(
36
+ node: ESTree.Node,
37
+ visitorKeys: VisitorKeys,
38
+ ): ReadonlySet<string> {
39
+ const names = new Set<string>();
40
+ let descendant: ESTree.Node = node;
41
+ let current: ESTree.Node | null = node;
42
+ while (current !== null && current.type !== "Program") {
43
+ if ("typeParameters" in current) {
44
+ for (const parameter of current.typeParameters?.params ?? []) {
45
+ names.add(parameter.name.name);
46
+ }
47
+ }
48
+ if (
49
+ current.type === "TSMappedType" &&
50
+ (descendant === current.nameType || descendant === current.typeAnnotation)
51
+ ) {
52
+ names.add(current.key.name);
53
+ }
54
+ if (current.type === "TSConditionalType" && descendant === current.trueType) {
55
+ collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
56
+ }
57
+ descendant = current;
58
+ current = current.parent;
59
+ }
60
+ return names;
61
+ }
@@ -0,0 +1,24 @@
1
+ import { resolveVariable } from "./scope.ts";
2
+
3
+ import type { ESTree, SourceCode } from "@oxlint/plugins";
4
+
5
+ function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean {
6
+ if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
7
+ if (sourceCode.isGlobalReference(expression)) return true;
8
+ const variable = resolveVariable(sourceCode, expression);
9
+ return variable === null || variable.defs.length === 0;
10
+ }
11
+
12
+ /** Reports whether a call target names one method on the global Reflect object. */
13
+ export function isGlobalReflectMethodCall(
14
+ sourceCode: SourceCode,
15
+ callee: ESTree.Expression,
16
+ methodName: string,
17
+ ): boolean {
18
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
19
+ if (!isGlobalReflect(sourceCode, callee.object)) return false;
20
+ const property = callee.property;
21
+ return callee.computed
22
+ ? property.type === "Literal" && property.value === methodName
23
+ : property.type === "Identifier" && property.name === methodName;
24
+ }
@@ -0,0 +1,15 @@
1
+ import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
2
+
3
+ /** Resolve an identifier to its binding by walking lexical scopes upward. */
4
+ export function resolveVariable(
5
+ sourceCode: SourceCode,
6
+ identifier: ESTree.IdentifierReference,
7
+ ): Variable | null {
8
+ let scope: Scope | null = sourceCode.getScope(identifier);
9
+ while (scope !== null) {
10
+ const variable = scope.set.get(identifier.name);
11
+ if (variable !== undefined) return variable;
12
+ scope = scope.upper;
13
+ }
14
+ return null;
15
+ }
@@ -0,0 +1,250 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+
3
+ import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts";
4
+
5
+ type VisitorKeys = Readonly<Record<string, readonly string[]>>;
6
+ type TypeScope = ESTree.Node;
7
+
8
+ type TypeBinding = {
9
+ readonly alias: ESTree.TSTypeAliasDeclaration | null;
10
+ readonly name: string;
11
+ readonly scope: TypeScope;
12
+ };
13
+
14
+ type Substitution = {
15
+ readonly substitutions: Substitutions;
16
+ readonly type: ESTree.TSType;
17
+ };
18
+
19
+ type Substitutions = ReadonlyMap<string, Substitution>;
20
+
21
+ export type TypeAliasEnvironment = {
22
+ readonly aliases: readonly ESTree.TSTypeAliasDeclaration[];
23
+ readonly bindingsByName: ReadonlyMap<string, readonly TypeBinding[]>;
24
+ readonly visitorKeys: VisitorKeys;
25
+ };
26
+
27
+ export type ResolvedTypeMatcher = (
28
+ type: ESTree.TSType,
29
+ matches: (child: ESTree.TSType) => boolean,
30
+ ) => boolean;
31
+
32
+ const environmentsByProgram = new WeakMap<ESTree.Program, TypeAliasEnvironment>();
33
+
34
+ function isNode(value: unknown): value is ESTree.Node {
35
+ return (
36
+ typeof value === "object" &&
37
+ value !== null &&
38
+ "type" in value &&
39
+ typeof value.type === "string"
40
+ );
41
+ }
42
+
43
+ function enclosingTypeScope(node: ESTree.Node): TypeScope {
44
+ let current: ESTree.Node | null = node.parent;
45
+ while (current !== null) {
46
+ if (
47
+ current.type === "Program" ||
48
+ current.type === "BlockStatement" ||
49
+ current.type === "TSModuleBlock" ||
50
+ current.type === "StaticBlock" ||
51
+ current.type === "SwitchStatement"
52
+ ) {
53
+ return current;
54
+ }
55
+ current = current.parent;
56
+ }
57
+ return node;
58
+ }
59
+
60
+ function declaredTypeBinding(node: ESTree.Node): {
61
+ readonly alias: ESTree.TSTypeAliasDeclaration | null;
62
+ readonly name: string;
63
+ } | null {
64
+ if (node.type === "TSTypeAliasDeclaration") {
65
+ return { alias: node, name: node.id.name };
66
+ }
67
+ if (
68
+ node.type === "TSInterfaceDeclaration" ||
69
+ node.type === "TSEnumDeclaration" ||
70
+ node.type === "ClassDeclaration" ||
71
+ node.type === "ClassExpression"
72
+ ) {
73
+ return node.id === null ? null : { alias: null, name: node.id.name };
74
+ }
75
+ if (
76
+ node.type === "ImportSpecifier" ||
77
+ node.type === "ImportDefaultSpecifier" ||
78
+ node.type === "ImportNamespaceSpecifier"
79
+ ) {
80
+ return { alias: null, name: node.local.name };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function collectTypeBindings(
86
+ node: ESTree.Node,
87
+ visitorKeys: VisitorKeys,
88
+ bindingsByName: Map<string, TypeBinding[]>,
89
+ aliases: ESTree.TSTypeAliasDeclaration[],
90
+ ): void {
91
+ const declared = declaredTypeBinding(node);
92
+ if (declared !== null) {
93
+ const bindings = bindingsByName.get(declared.name) ?? [];
94
+ bindings.push({ ...declared, scope: enclosingTypeScope(node) });
95
+ bindingsByName.set(declared.name, bindings);
96
+ if (declared.alias !== null) aliases.push(declared.alias);
97
+ }
98
+
99
+ // SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
100
+ const fields = node as unknown as Readonly<Record<string, unknown>>;
101
+ for (const key of visitorKeys[node.type] ?? []) {
102
+ const value = fields[key];
103
+ if (isNode(value)) {
104
+ collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
105
+ continue;
106
+ }
107
+ if (!Array.isArray(value)) continue;
108
+ for (const child of value) {
109
+ if (isNode(child)) {
110
+ collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /** Collect every lexical type alias and competing type binding in a program. */
117
+ export function createTypeAliasEnvironment(
118
+ program: ESTree.Program,
119
+ visitorKeys: VisitorKeys,
120
+ ): TypeAliasEnvironment {
121
+ const cached = environmentsByProgram.get(program);
122
+ if (cached !== undefined) return cached;
123
+ const bindingsByName = new Map<string, TypeBinding[]>();
124
+ const aliases: ESTree.TSTypeAliasDeclaration[] = [];
125
+ collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
126
+ const environment = { aliases, bindingsByName, visitorKeys };
127
+ environmentsByProgram.set(program, environment);
128
+ return environment;
129
+ }
130
+
131
+ function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null {
132
+ let current: ESTree.Node | null = node;
133
+ let distance = 0;
134
+ while (current !== null) {
135
+ if (current === ancestor) return distance;
136
+ current = current.parent;
137
+ distance += 1;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function nearestTypeBindings(
143
+ name: string,
144
+ use: ESTree.Node,
145
+ environment: TypeAliasEnvironment,
146
+ ): readonly TypeBinding[] {
147
+ const candidates = environment.bindingsByName.get(name) ?? [];
148
+ let nearestDistance = Number.POSITIVE_INFINITY;
149
+ let nearest: TypeBinding[] = [];
150
+ for (const candidate of candidates) {
151
+ const distance = ancestorDistance(candidate.scope, use);
152
+ if (distance === null || distance > nearestDistance) continue;
153
+ if (distance === nearestDistance) {
154
+ nearest.push(candidate);
155
+ continue;
156
+ }
157
+ nearestDistance = distance;
158
+ nearest = [candidate];
159
+ }
160
+ return nearest;
161
+ }
162
+
163
+ /** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
164
+ export function visibleTypeAlias(
165
+ name: string,
166
+ use: ESTree.Node,
167
+ environment: TypeAliasEnvironment,
168
+ ): ESTree.TSTypeAliasDeclaration | null {
169
+ if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
170
+ const bindings = nearestTypeBindings(name, use, environment);
171
+ return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null;
172
+ }
173
+
174
+ /** Return whether a local declaration shadows a built-in type at this use. */
175
+ export function hasVisibleTypeBinding(
176
+ name: string,
177
+ use: ESTree.Node,
178
+ environment: TypeAliasEnvironment,
179
+ ): boolean {
180
+ return (
181
+ lexicalTypeParameterNames(use, environment.visitorKeys).has(name) ||
182
+ nearestTypeBindings(name, use, environment).length > 0
183
+ );
184
+ }
185
+
186
+ function typeReferenceName(type: ESTree.TSTypeReference): string | null {
187
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
188
+ }
189
+
190
+ function aliasSubstitutions(
191
+ alias: ESTree.TSTypeAliasDeclaration,
192
+ reference: ESTree.TSTypeReference,
193
+ base: Substitutions,
194
+ ): Substitutions | null {
195
+ const parameters = alias.typeParameters?.params ?? [];
196
+ const arguments_ = reference.typeArguments?.params ?? [];
197
+ const next = new Map(base);
198
+ for (const [index, parameter] of parameters.entries()) {
199
+ const explicitArgument = arguments_[index];
200
+ const argument = explicitArgument ?? parameter.default;
201
+ if (argument === null || argument === undefined) return null;
202
+ const argumentSubstitutions = explicitArgument === undefined ? next : base;
203
+ next.set(parameter.name.name, {
204
+ type: argument,
205
+ substitutions: new Map(argumentSubstitutions),
206
+ });
207
+ }
208
+ return next;
209
+ }
210
+
211
+ /** Match a type after resolving visible aliases and substituting their type parameters. */
212
+ export function resolvedTypeMatches(
213
+ type: ESTree.TSType,
214
+ environment: TypeAliasEnvironment,
215
+ matcher: ResolvedTypeMatcher,
216
+ ): boolean {
217
+ const evaluate = (
218
+ current: ESTree.TSType,
219
+ substitutions: Substitutions,
220
+ resolvingAliases: ReadonlySet<ESTree.TSTypeAliasDeclaration>,
221
+ ): boolean => {
222
+ if (current.type === "TSTypeReference") {
223
+ const name = typeReferenceName(current);
224
+ if (name !== null) {
225
+ const substitution = substitutions.get(name);
226
+ if (substitution !== undefined && !current.typeArguments?.params.length) {
227
+ return evaluate(
228
+ substitution.type,
229
+ substitution.substitutions,
230
+ resolvingAliases,
231
+ );
232
+ }
233
+ const alias = visibleTypeAlias(name, current, environment);
234
+ if (alias !== null && !resolvingAliases.has(alias)) {
235
+ const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
236
+ if (nextSubstitutions !== null) {
237
+ const nextResolving = new Set(resolvingAliases);
238
+ nextResolving.add(alias);
239
+ return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return matcher(current, (child) =>
245
+ evaluate(child, substitutions, resolvingAliases),
246
+ );
247
+ };
248
+
249
+ return evaluate(type, new Map(), new Set());
250
+ }
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
4
+ Copyright (c) 2023-PRESENT ESLint Stylistic contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Vendored padding-line-between-statements
2
+
3
+ Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`.
4
+
5
+ Copied files:
6
+
7
+ - `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts`
8
+ - `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts`
9
+ - Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply.
10
+
11
+ The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, including skill assets. No Stylistic, ESLint, TypeScript-ESLint, or additional parser runtime dependency is required.
12
+
13
+ ## Local adaptations
14
+
15
+ - Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately.
16
+ - Replace `AST_NODE_TYPES` enum members with identical string literals.
17
+ - Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions.
18
+ - Replace the repository-specific `createRule` factory with `createPaddingLineRule(options)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options.
19
+ - Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads.
20
+ - Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code.
21
+
22
+ The opinionated policy lives outside this directory in `../../rules/require-readable-spacing.ts`. It adds spacing without collapsing existing blank lines. Short local bindings, consecutive imports, and adjacent overload signatures/implementation remain grouped. Spacing is syntactic, not an inference of business-logic boundaries.
23
+
24
+ ## Updating and verification
25
+
26
+ Fetch an explicit upstream revision, compare the original rule and types against this revision, and port relevant fixes while retaining the adapters above. Update this record and preserve the license. Run `pnpm check` and `pnpm sync:skill-assets` as required by repository guidance.
27
+
28
+ Focused Oxlint RuleTester cases live in `../../rules/require-readable-spacing.test.ts`; they test exact fixes, JSDoc/trailing comments, same-line statements, semicolon-free code, TypeScript exports/overloads, Effect-style generators, and upstream removal behavior. `../../rules/require-readable-spacing-cli.test.ts` verifies the exported plugin through the native Oxlint CLI on multiple files, including rejection, autofix, and repeated-fix stability. The complete upstream JS/TS test suites have not been ported; this is focused compatibility evidence, not a claim of full upstream conformance.
@@ -0,0 +1,51 @@
1
+ // Local replacements for the upstream helper imports. See UPSTREAM.md.
2
+ import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins";
3
+
4
+ type Token = SyntaxToken | Comment;
5
+
6
+ /** Line terminators recognized by the upstream padding matcher. */
7
+ export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
8
+
9
+ /** Test a closing brace without treating comment text as punctuation. */
10
+ export const isClosingBraceToken = (token: Token): boolean =>
11
+ token.type === "Punctuator" && token.value === "}";
12
+
13
+ /** Test a semicolon without treating comment text as punctuation. */
14
+ export const isSemicolonToken = (token: Token): boolean =>
15
+ token.type === "Punctuator" && token.value === ";";
16
+
17
+ /** Filter the optional final semicolon when identifying block-like statements. */
18
+ export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token);
19
+
20
+ /** Compare token/node boundaries, including attached comments. */
21
+ export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean =>
22
+ left.loc.end.line === right.loc.start.line;
23
+
24
+ /** Recognize declarations and expressions used by the upstream IIFE matcher. */
25
+ export const isFunction = (node: ESTree.Node): boolean =>
26
+ node.type === "FunctionDeclaration" ||
27
+ node.type === "FunctionExpression" ||
28
+ node.type === "ArrowFunctionExpression";
29
+
30
+ /** Preserve the upstream multiline statement heuristic. */
31
+ export const isSingleLine = (node: ESTree.Node): boolean =>
32
+ node.loc.start.line === node.loc.end.line;
33
+
34
+ /** Unwrap optional chaining before checking IIFE syntax. */
35
+ export const skipChainExpression = (node: ESTree.Node): ESTree.Node =>
36
+ node.type === "ChainExpression" ? node.expression : node;
37
+
38
+ /** Only a program or function-body expression can begin a directive prologue. */
39
+ export const isTopLevelExpressionStatement = (
40
+ node: ESTree.Node,
41
+ ): node is ESTree.ExpressionStatement =>
42
+ node.type === "ExpressionStatement" &&
43
+ (node.parent.type === "Program" ||
44
+ (node.parent.type === "BlockStatement" && isFunction(node.parent.parent)));
45
+
46
+ /** A single wrapping pair suffices to exclude a string from directive syntax. */
47
+ export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean {
48
+ const before = sourceCode.getTokenBefore(node);
49
+ const after = sourceCode.getTokenAfter(node);
50
+ return before?.value === "(" && after?.value === ")";
51
+ }