ts-codemod-lib 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/entry-point.mjs +2 -24
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +330 -292
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +85 -70
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +971 -838
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +32 -9
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +299 -134
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
- package/dist/functions/ast-transformers/index.mjs +10 -12
- package/dist/functions/ast-transformers/index.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +14 -7
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +12 -15
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +70 -59
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -5
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +15 -20
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +30 -34
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +197 -156
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.mjs +74 -75
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/ast-transformers/types.mjs +2 -2
- package/dist/functions/ast-transformers/types.mjs.map +1 -1
- package/dist/functions/constants/ignore-comment-text.mjs +11 -13
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -1
- package/dist/functions/constants/index.mjs +2 -2
- package/dist/functions/constants/index.mjs.map +1 -1
- package/dist/functions/functions/has-call-or-construct-signature.d.mts +18 -0
- package/dist/functions/functions/has-call-or-construct-signature.d.mts.map +1 -0
- package/dist/functions/functions/has-call-or-construct-signature.mjs +87 -0
- package/dist/functions/functions/has-call-or-construct-signature.mjs.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs +67 -38
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/index.d.mts +1 -0
- package/dist/functions/functions/index.d.mts.map +1 -1
- package/dist/functions/functions/index.mjs +12 -11
- package/dist/functions/functions/index.mjs.map +1 -1
- package/dist/functions/functions/is-as-const-node.mjs +25 -20
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.mjs +245 -25
- package/dist/functions/functions/is-atomic-type-node.mjs.map +1 -1
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs +34 -13
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +120 -14
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/is-spread-parameter-node.mjs +5 -6
- package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -1
- package/dist/functions/functions/remove-parentheses.mjs +4 -4
- package/dist/functions/functions/remove-parentheses.mjs.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +23 -9
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
- package/dist/functions/functions/unwrap-readonly.mjs +3 -4
- package/dist/functions/functions/unwrap-readonly.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts +14 -5
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +67 -30
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/index.mjs +5 -24
- package/dist/functions/index.mjs.map +1 -1
- package/dist/functions/utils/index.mjs +2 -2
- package/dist/functions/utils/index.mjs.map +1 -1
- package/dist/functions/utils/replace-with-debug.mjs +4 -6
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
- package/dist/index.mjs +2 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/functions/ast-transformers/convert-to-readonly.mts +19 -13
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +129 -1
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +32 -9
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +76 -22
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +8 -1
- package/src/functions/functions/has-call-or-construct-signature.mts +120 -0
- package/src/functions/functions/index.mts +1 -0
- package/src/functions/functions/wrap-with-parentheses.mts +32 -17
|
@@ -12,8 +12,12 @@ import { transformSourceCode } from './transform-source-code.mjs';
|
|
|
12
12
|
* Declarations every case is prefixed with, so that each `source` can be a
|
|
13
13
|
* single expression statement. `declare const` keeps them free of index
|
|
14
14
|
* accesses of their own.
|
|
15
|
+
*
|
|
16
|
+
* `console` is among them because {@link project} loads only the libraries
|
|
17
|
+
* these cases need, and the one that declares `console` is not one of them.
|
|
15
18
|
*/
|
|
16
19
|
const preamble = dedent`
|
|
20
|
+
declare const console: { log: (...args: readonly unknown[]) => void };
|
|
17
21
|
declare const xs: readonly number[];
|
|
18
22
|
declare const mut_ys: number[];
|
|
19
23
|
declare const pair: readonly [number, number];
|
|
@@ -47,9 +51,7 @@ const testFn = async ({
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
const transformed = await formatter(
|
|
50
|
-
|
|
51
|
-
enableNoUncheckedIndexedAccessTransformer({ ...options, debug }),
|
|
52
|
-
]),
|
|
54
|
+
transformInProject(`${preamble}\n\n${source}`, { ...options, debug }),
|
|
53
55
|
);
|
|
54
56
|
|
|
55
57
|
const expectedFormatted = await formatter(`${preamble}\n\n${expected}`);
|
|
@@ -57,6 +59,54 @@ const testFn = async ({
|
|
|
57
59
|
expect(transformed).toBe(expectedFormatted);
|
|
58
60
|
};
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Runs the transformer over `code` in {@link project}, and returns what it
|
|
64
|
+
* made of it.
|
|
65
|
+
*/
|
|
66
|
+
const transformInProject = (
|
|
67
|
+
code: string,
|
|
68
|
+
options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
|
|
69
|
+
): string => {
|
|
70
|
+
const sourceAst = project.createSourceFile('source.ts', code, {
|
|
71
|
+
overwrite: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
|
|
75
|
+
|
|
76
|
+
return sourceAst.getFullText();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The one project every case is checked in, rather than one project per case.
|
|
81
|
+
*
|
|
82
|
+
* `transformSourceCode` builds a project per call, and this transformer checks
|
|
83
|
+
* the file twice — once with `noUncheckedIndexedAccess` on and once with it
|
|
84
|
+
* off. A case that went through it therefore paid for the whole default
|
|
85
|
+
* library, `lib.esnext.full.d.ts` with the DOM inside it, being parsed and
|
|
86
|
+
* checked from scratch: ~300 ms a case, of which the file under test was a
|
|
87
|
+
* fraction of a millisecond. Sixty of those took `test:cov` past Vitest's
|
|
88
|
+
* timeout on CI.
|
|
89
|
+
*
|
|
90
|
+
* Reusing the project is half of the answer and the smaller half — measured,
|
|
91
|
+
* it takes a case from ~300 ms to ~260 ms, because what an edit invalidates is
|
|
92
|
+
* the program, not the parsed library behind it. Naming the libraries the
|
|
93
|
+
* cases actually need is the rest: together they bring a case to ~25 ms.
|
|
94
|
+
*
|
|
95
|
+
* What that leaves out — `transformSourceCode` itself, and the transformer
|
|
96
|
+
* against the full library — is what the tests at the bottom of this file
|
|
97
|
+
* still cover.
|
|
98
|
+
*/
|
|
99
|
+
const project = new tsm.Project({
|
|
100
|
+
useInMemoryFileSystem: true,
|
|
101
|
+
compilerOptions: {
|
|
102
|
+
target: tsm.ts.ScriptTarget.ESNext,
|
|
103
|
+
module: tsm.ts.ModuleKind.ESNext,
|
|
104
|
+
// `es2015` rather than `es5` for `Symbol.iterator`, which the spread and
|
|
105
|
+
// `for-of` cases need at this target.
|
|
106
|
+
lib: ['lib.es2015.d.ts'],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
60
110
|
const formatter = (code: string): Promise<string> =>
|
|
61
111
|
prettier.format(code, {
|
|
62
112
|
parser: 'typescript',
|
|
@@ -456,6 +506,8 @@ describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
|
456
506
|
|
|
457
507
|
test('the errors the option introduces are gone from the result', () => {
|
|
458
508
|
const source = dedent`
|
|
509
|
+
declare const console: { log: (...args: readonly unknown[]) => void };
|
|
510
|
+
|
|
459
511
|
const parseRoute = (path: string): string => {
|
|
460
512
|
const parts = path.split('/');
|
|
461
513
|
|
|
@@ -484,11 +536,7 @@ describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
|
484
536
|
expect(diagnosticsUnderTheOption(source).length).toBeGreaterThan(0);
|
|
485
537
|
|
|
486
538
|
assert.deepStrictEqual(
|
|
487
|
-
diagnosticsUnderTheOption(
|
|
488
|
-
transformSourceCode(source, false, [
|
|
489
|
-
enableNoUncheckedIndexedAccessTransformer(),
|
|
490
|
-
]),
|
|
491
|
-
),
|
|
539
|
+
diagnosticsUnderTheOption(transformInProject(source)),
|
|
492
540
|
[],
|
|
493
541
|
);
|
|
494
542
|
});
|
|
@@ -542,19 +590,9 @@ describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
|
542
590
|
});
|
|
543
591
|
|
|
544
592
|
/** The compiler's own verdict on the code, with the option this transformer prepares for turned on. */
|
|
545
|
-
const diagnosticsUnderTheOption = (code: string): readonly string[] =>
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
compilerOptions: {
|
|
549
|
-
target: tsm.ts.ScriptTarget.ESNext,
|
|
550
|
-
module: tsm.ts.ModuleKind.ESNext,
|
|
551
|
-
strict: true,
|
|
552
|
-
noUncheckedIndexedAccess: true,
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
return project
|
|
557
|
-
.createSourceFile('source.ts', code)
|
|
593
|
+
const diagnosticsUnderTheOption = (code: string): readonly string[] =>
|
|
594
|
+
diagnosticsProject
|
|
595
|
+
.createSourceFile('source.ts', code, { overwrite: true })
|
|
558
596
|
.getPreEmitDiagnostics()
|
|
559
597
|
.map((diagnostic) => diagnostic.getMessageText())
|
|
560
598
|
.map((messageText) =>
|
|
@@ -562,4 +600,20 @@ const diagnosticsUnderTheOption = (code: string): readonly string[] => {
|
|
|
562
600
|
? messageText
|
|
563
601
|
: messageText.getMessageText(),
|
|
564
602
|
);
|
|
565
|
-
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* A second project, because {@link project} has to start with the option off,
|
|
606
|
+
* and reused for the reason that one is. `getPreEmitDiagnostics` checks the
|
|
607
|
+
* whole program rather than one expression, so the library it is pointed at
|
|
608
|
+
* costs more here than anywhere else in this file.
|
|
609
|
+
*/
|
|
610
|
+
const diagnosticsProject = new tsm.Project({
|
|
611
|
+
useInMemoryFileSystem: true,
|
|
612
|
+
compilerOptions: {
|
|
613
|
+
target: tsm.ts.ScriptTarget.ESNext,
|
|
614
|
+
module: tsm.ts.ModuleKind.ESNext,
|
|
615
|
+
lib: ['lib.es2015.d.ts'],
|
|
616
|
+
strict: true,
|
|
617
|
+
noUncheckedIndexedAccess: true,
|
|
618
|
+
},
|
|
619
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as tsm from 'ts-morph';
|
|
2
2
|
import {
|
|
3
|
+
hasCallOrConstructSignature,
|
|
3
4
|
isAtomicTypeNode,
|
|
4
5
|
isReadonlyTupleOrArrayTypeNode,
|
|
5
6
|
isReadonlyTypeReferenceNode,
|
|
@@ -121,8 +122,14 @@ export const groupUnionIntersectionTypes = (
|
|
|
121
122
|
continue;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// A type literal that declares a call or construct signature is deliberately
|
|
126
|
+
// not grouped here: this group is joined and wrapped in a single
|
|
127
|
+
// `Readonly<...>`, which such a literal does not survive. It falls through
|
|
128
|
+
// to `others`, which leaves it as written. See
|
|
129
|
+
// `hasCallOrConstructSignature`.
|
|
124
130
|
if (
|
|
125
|
-
t.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
131
|
+
(t.isKind(tsm.SyntaxKind.TypeLiteral) &&
|
|
132
|
+
!hasCallOrConstructSignature(t)) ||
|
|
126
133
|
isReadonlyTypeReferenceNode(t)
|
|
127
134
|
) {
|
|
128
135
|
if (mut_grouped.typeLiterals === undefined) {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as tsm from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines whether a node is a type literal or an interface that declares a
|
|
5
|
+
* call or construct signature.
|
|
6
|
+
*
|
|
7
|
+
* `Readonly<T>` is a mapped type over `keyof T`, and neither signature kind is a
|
|
8
|
+
* property, so neither survives the mapping: wrapping `{ (x: string): void }`
|
|
9
|
+
* with `Readonly` produces a type that is no longer callable. A type mixing
|
|
10
|
+
* properties with a call signature is worse still — the properties come through
|
|
11
|
+
* and only the signature disappears. So a type literal carrying one must not be
|
|
12
|
+
* wrapped; its members are marked `readonly` in place instead.
|
|
13
|
+
*
|
|
14
|
+
* @param node - The node to check
|
|
15
|
+
* @returns true if the node declares at least one call or construct signature,
|
|
16
|
+
* false for any other node kind
|
|
17
|
+
*/
|
|
18
|
+
export const hasCallOrConstructSignature = (node: tsm.Node): boolean =>
|
|
19
|
+
(node.isKind(tsm.SyntaxKind.TypeLiteral) ||
|
|
20
|
+
node.isKind(tsm.SyntaxKind.InterfaceDeclaration)) &&
|
|
21
|
+
node
|
|
22
|
+
.getMembers()
|
|
23
|
+
.some(
|
|
24
|
+
(member) =>
|
|
25
|
+
member.isKind(tsm.SyntaxKind.CallSignature) ||
|
|
26
|
+
member.isKind(tsm.SyntaxKind.ConstructSignature),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (import.meta.vitest !== undefined) {
|
|
30
|
+
const getFirstNodeOfKind = (
|
|
31
|
+
sourceCode: string,
|
|
32
|
+
kind: tsm.SyntaxKind,
|
|
33
|
+
): tsm.Node => {
|
|
34
|
+
const project = new tsm.Project({ useInMemoryFileSystem: true });
|
|
35
|
+
|
|
36
|
+
const sourceFile = project.createSourceFile('test.ts', sourceCode);
|
|
37
|
+
|
|
38
|
+
return sourceFile.getFirstDescendantByKindOrThrow(kind);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getTypeLiteral = (sourceCode: string): tsm.Node =>
|
|
42
|
+
getFirstNodeOfKind(sourceCode, tsm.SyntaxKind.TypeLiteral);
|
|
43
|
+
|
|
44
|
+
describe('hasCallOrConstructSignature', () => {
|
|
45
|
+
test.each([
|
|
46
|
+
{ name: 'call signature', code: 'type T = { (a: string): void };' },
|
|
47
|
+
{ name: 'construct signature', code: 'type T = { new (a: string): X };' },
|
|
48
|
+
{
|
|
49
|
+
name: 'call signature mixed with a property',
|
|
50
|
+
code: 'type T = { (a: string): void; b: number };',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'overloaded call signatures',
|
|
54
|
+
code: 'type T = { (a: string): void; (a: number): void };',
|
|
55
|
+
},
|
|
56
|
+
])('is true for a type literal with a $name', ({ code }) => {
|
|
57
|
+
assert.isTrue(hasCallOrConstructSignature(getTypeLiteral(code)));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test.each([
|
|
61
|
+
{ name: 'properties only', code: 'type T = { a: number; b: string };' },
|
|
62
|
+
{ name: 'an index signature', code: 'type T = { [k: string]: number };' },
|
|
63
|
+
{
|
|
64
|
+
name: 'a method signature',
|
|
65
|
+
code: 'type T = { a(b: string): void };',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'a function-typed property',
|
|
69
|
+
code: 'type T = { a: (b: string) => void };',
|
|
70
|
+
},
|
|
71
|
+
{ name: 'no members', code: 'type T = {};' },
|
|
72
|
+
])('is false for a type literal with $name', ({ code }) => {
|
|
73
|
+
assert.isFalse(hasCallOrConstructSignature(getTypeLiteral(code)));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('is true for an interface with a call signature', () => {
|
|
77
|
+
assert.isTrue(
|
|
78
|
+
hasCallOrConstructSignature(
|
|
79
|
+
getFirstNodeOfKind(
|
|
80
|
+
'interface I { (a: string): void }',
|
|
81
|
+
tsm.SyntaxKind.InterfaceDeclaration,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('is false for an interface without one', () => {
|
|
88
|
+
assert.isFalse(
|
|
89
|
+
hasCallOrConstructSignature(
|
|
90
|
+
getFirstNodeOfKind(
|
|
91
|
+
'interface I { a: string }',
|
|
92
|
+
tsm.SyntaxKind.InterfaceDeclaration,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test.each([
|
|
99
|
+
{
|
|
100
|
+
name: 'a function type',
|
|
101
|
+
code: 'type T = (a: string) => void;',
|
|
102
|
+
kind: tsm.SyntaxKind.FunctionType,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'a constructor type',
|
|
106
|
+
code: 'type T = new (a: string) => X;',
|
|
107
|
+
kind: tsm.SyntaxKind.ConstructorType,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'a class declaration',
|
|
111
|
+
code: 'class C { constructor(a: string) {} }',
|
|
112
|
+
kind: tsm.SyntaxKind.ClassDeclaration,
|
|
113
|
+
},
|
|
114
|
+
])('is false for $name', ({ code, kind }) => {
|
|
115
|
+
assert.isFalse(
|
|
116
|
+
hasCallOrConstructSignature(getFirstNodeOfKind(code, kind)),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if a string is already wrapped with a single pair of balanced parentheses
|
|
3
|
-
* that encompasses the entire expression.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* isWrappedWithParentheses('(A)') // true
|
|
7
|
-
* isWrappedWithParentheses('((A))') // true (outer pair wraps everything)
|
|
8
|
-
* isWrappedWithParentheses('(A) | (B)') // false (outer parens don't wrap everything)
|
|
9
|
-
* isWrappedWithParentheses('A') // false
|
|
10
|
-
*/
|
|
11
1
|
import { asUint32, range } from 'ts-data-forge';
|
|
12
2
|
|
|
13
|
-
const parenDepthDelta = (char: string): number => {
|
|
3
|
+
const parenDepthDelta = (char: string | undefined): number => {
|
|
14
4
|
switch (char) {
|
|
15
5
|
case '(':
|
|
16
6
|
return 1;
|
|
@@ -18,11 +8,27 @@ const parenDepthDelta = (char: string): number => {
|
|
|
18
8
|
case ')':
|
|
19
9
|
return -1;
|
|
20
10
|
|
|
11
|
+
// `at` returns `undefined` past the end of the string. The loop below
|
|
12
|
+
// never asks for such an index, but the type says it can.
|
|
13
|
+
case undefined:
|
|
21
14
|
default:
|
|
22
15
|
return 0;
|
|
23
16
|
}
|
|
24
17
|
};
|
|
25
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a string is already wrapped with a single pair of balanced
|
|
21
|
+
* parentheses that encompasses the entire expression.
|
|
22
|
+
*
|
|
23
|
+
* `'(A)'` and `'((A))'` are wrapped — in the second the outer pair still spans
|
|
24
|
+
* everything — while `'A'` is not, and neither is `'(A) | (B)'`, whose first
|
|
25
|
+
* parenthesis closes before the end.
|
|
26
|
+
*
|
|
27
|
+
* Module-local, so `samples/` — which imports the package the way a consumer
|
|
28
|
+
* does — cannot reach it, and it therefore carries no `@example`. See
|
|
29
|
+
* {@link wrapWithParentheses}, the exported form, whose example covers the
|
|
30
|
+
* same cases.
|
|
31
|
+
*/
|
|
26
32
|
const isWrappedWithParentheses = (str: string): boolean => {
|
|
27
33
|
const trimmed = str.trim();
|
|
28
34
|
|
|
@@ -35,7 +41,7 @@ const isWrappedWithParentheses = (str: string): boolean => {
|
|
|
35
41
|
let mut_depth = 0;
|
|
36
42
|
|
|
37
43
|
for (const mut_i of range(0, asUint32(trimmed.length))) {
|
|
38
|
-
mut_depth += parenDepthDelta(trimmed.
|
|
44
|
+
mut_depth += parenDepthDelta(trimmed.at(mut_i));
|
|
39
45
|
|
|
40
46
|
// If we reach depth 0 before the end, the outer parentheses don't wrap everything
|
|
41
47
|
if (mut_depth === 0 && mut_i < trimmed.length - 1) {
|
|
@@ -52,11 +58,20 @@ const isWrappedWithParentheses = (str: string): boolean => {
|
|
|
52
58
|
* fully wrapped with balanced parentheses.
|
|
53
59
|
*
|
|
54
60
|
* @example
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* wrapWithParentheses('A
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* assert.deepStrictEqual(wrapWithParentheses('A'), '(A)');
|
|
64
|
+
*
|
|
65
|
+
* // already wrapped, so not '((A))'
|
|
66
|
+
* assert.deepStrictEqual(wrapWithParentheses('(A)'), '(A)');
|
|
67
|
+
*
|
|
68
|
+
* assert.deepStrictEqual(wrapWithParentheses('A | B'), '(A | B)');
|
|
69
|
+
*
|
|
70
|
+
* assert.deepStrictEqual(wrapWithParentheses('(A | B)'), '(A | B)');
|
|
71
|
+
*
|
|
72
|
+
* // the outer parentheses do not span everything, so a pair is added
|
|
73
|
+
* assert.deepStrictEqual(wrapWithParentheses('(A) | (B)'), '((A) | (B))');
|
|
74
|
+
* ```
|
|
60
75
|
*/
|
|
61
76
|
export const wrapWithParentheses = (nodeStr: string): string => {
|
|
62
77
|
const trimmed = nodeStr.trim();
|