From 9a949884284d79ace712035187118770413ca390 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 25 Sep 2024 16:40:41 -0400 Subject: [PATCH 1/3] Ast node clone (#1281) * Statement.ts and BscType classes are done * Finish expression clone, fix lint issues * Add lots of statement tests * Finish all statement clone tests * Coverage for all expressions * Fix most failing tests. * Fix tests, properly reparent nodes * Fix npm audit issue --- package-lock.json | 26 +- package.json | 2 +- src/astUtils/reflection.ts | 13 +- src/astUtils/visitors.ts | 2 +- src/parser/AstNode.spec.ts | 1494 +++++++++++++++++++++++++++++++- src/parser/AstNode.ts | 34 +- src/parser/Expression.ts | 330 ++++++- src/parser/Statement.spec.ts | 1 - src/parser/Statement.ts | 485 ++++++++++- src/types/ArrayType.ts | 4 + src/types/BooleanType.ts | 4 + src/types/BscType.ts | 1 + src/types/CustomType.ts | 4 + src/types/DoubleType.ts | 4 + src/types/DynamicType.ts | 4 + src/types/FloatType.ts | 4 + src/types/FunctionType.ts | 10 + src/types/IntegerType.ts | 4 + src/types/InterfaceType.ts | 10 + src/types/InvalidType.ts | 4 + src/types/LongIntegerType.ts | 4 + src/types/ObjectType.ts | 4 + src/types/StringType.ts | 4 + src/types/UninitializedType.ts | 4 + src/types/VoidType.ts | 4 + src/util.ts | 33 + 26 files changed, 2447 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffd1a6ed3..154d415f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ "rimraf": "^2.7.1", "semver-extra": "^3.0.0", "sinon": "^9.0.2", - "source-map-support": "^0.5.13", + "source-map-support": "^0.5.21", "sync-request": "^6.1.0", "testdouble": "^3.5.2", "thenby": "^1.3.4", @@ -2209,9 +2209,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001599", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", - "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", "dev": true, "funding": [ { @@ -8066,9 +8066,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "dependencies": { "buffer-from": "^1.0.0", @@ -10795,9 +10795,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001599", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", - "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", "dev": true }, "caseless": { @@ -15165,9 +15165,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", diff --git a/package.json b/package.json index ea7634dac..92e726ddf 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "rimraf": "^2.7.1", "semver-extra": "^3.0.0", "sinon": "^9.0.2", - "source-map-support": "^0.5.13", + "source-map-support": "^0.5.21", "sync-request": "^6.1.0", "testdouble": "^3.5.2", "thenby": "^1.3.4", diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index d262dbbfd..9556f2a5e 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -1,5 +1,5 @@ -import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement } from '../parser/Statement'; -import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypeCastExpression } from '../parser/Expression'; +import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement } from '../parser/Statement'; +import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypeCastExpression, TernaryExpression, NullCoalescingExpression } from '../parser/Expression'; import type { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; import type { BscFile, File, TypedefProvider } from '../interfaces'; @@ -91,6 +91,12 @@ export function isLabelStatement(element: AstNode | undefined): element is Label export function isReturnStatement(element: AstNode | undefined): element is ReturnStatement { return element?.constructor?.name === 'ReturnStatement'; } +export function isTernaryExpression(element: AstNode | undefined): element is TernaryExpression { + return element?.constructor?.name === 'TernaryExpression'; +} +export function isNullCoalescingExpression(element: AstNode | undefined): element is NullCoalescingExpression { + return element?.constructor?.name === 'NullCoalescingExpression'; +} export function isEndStatement(element: AstNode | undefined): element is EndStatement { return element?.constructor?.name === 'EndStatement'; } @@ -106,6 +112,9 @@ export function isForEachStatement(element: AstNode | undefined): element is For export function isWhileStatement(element: AstNode | undefined): element is WhileStatement { return element?.constructor?.name === 'WhileStatement'; } +export function isDimStatement(element: AstNode | undefined): element is DimStatement { + return element?.constructor?.name === 'DimStatement'; +} export function isDottedSetStatement(element: AstNode | undefined): element is DottedSetStatement { return element?.constructor?.name === 'DottedSetStatement'; } diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts index 6426f4d1c..254b9ddb2 100644 --- a/src/astUtils/visitors.ts +++ b/src/astUtils/visitors.ts @@ -78,7 +78,7 @@ export function walk(owner: T, key: keyof T, visitor: WalkVisitor, options: W * @param filter a function used to filter items from the array. return true if that item should be walked */ export function walkArray(array: Array, visitor: WalkVisitor, options: WalkOptions, parent?: AstNode, filter?: (element: T) => boolean) { - for (let i = 0; i < array.length; i++) { + for (let i = 0; i < array?.length; i++) { if (!filter || filter(array[i])) { const startLength = array.length; walk(array, i, visitor, options, parent); diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 0f997f0c9..9b1ecb13a 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -3,12 +3,16 @@ import * as fsExtra from 'fs-extra'; import { Program } from '../Program'; import type { BrsFile } from '../files/BrsFile'; import { expect } from '../chai-config.spec'; -import type { DottedGetExpression } from './Expression'; +import type { AALiteralExpression, AAMemberExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, GroupingExpression, IndexedGetExpression, NewExpression, NullCoalescingExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, TypeCastExpression, UnaryExpression, XmlAttributeGetExpression } from './Expression'; import { expectZeroDiagnostics } from '../testHelpers.spec'; import { tempDir, rootDir, stagingDir } from '../testHelpers.spec'; -import { isAssignmentStatement, isClassStatement, isDottedGetExpression, isPrintStatement } from '../astUtils/reflection'; -import type { FunctionStatement } from './Statement'; -import { AssignmentStatement } from './Statement'; +import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isCallfuncExpression, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDimStatement, isDottedGetExpression, isDottedSetStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isGroupingExpression, isIfStatement, isIncrementStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isLibraryStatement, isMethodStatement, isNamespaceStatement, isNewExpression, isNullCoalescingExpression, isPrintStatement, isReturnStatement, isTaggedTemplateStringExpression, isTemplateStringExpression, isTemplateStringQuasiExpression, isTernaryExpression, isThrowStatement, isTryCatchStatement, isTypeCastExpression, isUnaryExpression, isWhileStatement, isXmlAttributeGetExpression } from '../astUtils/reflection'; +import type { ClassStatement, FunctionStatement, InterfaceFieldStatement, InterfaceMethodStatement, MethodStatement, InterfaceStatement, CatchStatement, ThrowStatement, EnumStatement, EnumMemberStatement, ConstStatement, Block, CommentStatement, PrintStatement, DimStatement, ForStatement, WhileStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, TryCatchStatement, DottedSetStatement } from './Statement'; +import { AssignmentStatement, EmptyStatement } from './Statement'; +import { ParseMode, Parser } from './Parser'; +import type { AstNode } from './AstNode'; + +type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; describe('AstNode', () => { let program: Program; @@ -184,4 +188,1486 @@ describe('AstNode', () => { expect(count).to.eql(1); }); }); + + describe('clone', () => { + function testClone(code: string | AstNode) { + let originalOuter: AstNode; + if (typeof code === 'string') { + const parser = Parser.parse(code, { mode: ParseMode.BrighterScript }); + originalOuter = parser.ast; + expectZeroDiagnostics(parser); + } else { + originalOuter = code; + } + + const cloneOuter = originalOuter.clone(); + //ensure the clone is identical to the original + + //compare them both ways to ensure no extra properties exist + ensureIdentical(originalOuter, cloneOuter); + ensureIdentical(cloneOuter, originalOuter); + + function ensureIdentical(original: AstNode, clone: AstNode, ancestors = [], seenNodes = new Map()) { + for (let key in original) { + let fullKey = [...ancestors, key].join('.'); + const originalValue = original?.[key]; + const cloneValue = clone?.[key]; + let typeOfValue = typeof originalValue; + + //skip these properties + if ( + ['parent', 'symbolTable', 'range'].includes(key) || + //this is a circular reference property or the `returnType` prop, skip it + (isFunctionExpression(original) && (key === 'functionStatement' || key === 'returnType')) || + //circular reference property for annotations + (isAnnotationExpression(original) && key === 'call') + ) { + continue; + } + + //if this is an object, recurse + if (typeOfValue === 'object' && originalValue !== null) { + //skip circular references (but give some tollerance) + if (seenNodes.get(originalValue) > 2) { + throw new Error(`${fullKey} is a circular reference`); + } + seenNodes.set(originalValue, (seenNodes.get(originalValue) ?? 0) + 1); + + //object references should not be the same + if (originalValue === cloneValue) { + throw new Error(`${fullKey} is the same object reference`); + } + //compare child object values + ensureIdentical(originalValue, cloneValue, [...ancestors, key], seenNodes); + + //for these tests, empty arrays can be the same as undefined so skip + } else if ( + (Array.isArray(originalValue) && originalValue.length === 0 && cloneValue === undefined) || + (Array.isArray(cloneValue) && cloneValue.length === 0 && originalValue === undefined)) { + continue; + + //these values must be identical + } else { + // eslint-disable-next-line no-useless-catch + try { + expect(cloneValue).to.equal(originalValue, `'${fullKey}' should be identical`); + } catch (e) { + //build a full list of ancestors for orig and clone + let originalChain = [originalOuter]; + let cloneChain = [cloneOuter]; + for (let key of fullKey.split('.')) { + originalChain.push(originalChain[originalChain.length - 1]?.[key]); + cloneChain.push(cloneChain[cloneChain.length - 1]?.[key]); + } + console.error((e as Error)?.message, fullKey, originalChain, cloneChain); + throw e; + } + } + } + } + } + + it('clones EmptyStatement', () => { + testClone(new EmptyStatement( + util.createRange(1, 2, 3, 4) + )); + }); + + it('clones body with undefined statements array', () => { + const original = Parser.parse(` + sub main() + end sub + `).ast; + original.statements = undefined; + testClone(original); + }); + + it('clones body with undefined in the statements array', () => { + const original = Parser.parse(` + sub main() + end sub + `).ast; + original.statements.push(undefined); + testClone(original); + }); + + it('clones interfaces', () => { + testClone(` + interface Empty + end interface + interface Movie + name as string + previous as Movie + sub play() + function play2(a, b as string) as dynamic + end interface + interface Short extends Movie + length as integer + end interface + `); + }); + + it('handles when interfaces are missing their body', () => { + const original = Parser.parse(` + interface Empty + end interface + `).ast; + original.findChild(isInterfaceStatement).body = undefined; + testClone(original); + }); + + it('handles when interfaces have undefined statements in the body', () => { + const original = Parser.parse(` + interface Empty + end interface + `).ast; + original.findChild(isInterfaceStatement).body.push(undefined); + testClone(original); + }); + + it('handles when interfaces have undefined field type', () => { + const original = Parser.parse(` + interface Empty + name as string + end interface + `).ast; + original.findChild(isInterfaceFieldStatement).type = undefined; + testClone(original); + }); + + it('handles when interface function has undefined param and return type', () => { + const original = Parser.parse(` + interface Empty + function test() as dynamic + end interface + `).ast; + original.findChild(isInterfaceMethodStatement).params.push(undefined); + original.findChild(isInterfaceMethodStatement).returnType = undefined; + testClone(original); + }); + + it('handles when interface function has undefined params array', () => { + const original = Parser.parse(` + interface Empty + function test(a) as dynamic + end interface + `).ast; + original.findChild(isInterfaceMethodStatement).params = undefined; + testClone(original); + }); + + it('clones empty class', () => { + testClone(` + class Movie + end class + `); + }); + + it('clones class with undefined body', () => { + const original = Parser.parse(` + class Movie + end class + `).ast; + original.findChild(isClassStatement).body = undefined; + testClone(original); + }); + + it('clones class with undefined body statement', () => { + const original = Parser.parse(` + class Movie + end class + `).ast; + original.findChild(isClassStatement).body.push(undefined); + testClone(original); + }); + + it('clones class having parent class', () => { + testClone(` + class Video + end class + class Movie extends Video + end class + `); + }); + + it('clones class', () => { + testClone(` + class Movie + name as string + previous as Movie + sub play() + end sub + function play2(a, b as string) as dynamic + end function + end class + `); + }); + + it('clones access modifiers', () => { + testClone(` + class Movie + public sub test() + end sub + protected name = "bob" + private child = {} + end class + `); + }); + + it('clones AssignmentStatement', () => { + testClone(` + sub main() + thing = true + end sub + `); + }); + + it('clones AssignmentStatement with missing value', () => { + const original = Parser.parse(` + sub main() + thing = true + end sub + `).ast; + original.findChild(isAssignmentStatement).value = undefined; + testClone(original); + }); + + it('clones Block with undefined statements array', () => { + const original = Parser.parse(` + sub main() + thing = true + end sub + `).ast; + original.findChild(isBlock).statements = undefined; + testClone(original); + }); + + it('clones Block with undefined statement in statements array', () => { + const original = Parser.parse(` + sub main() + thing = true + end sub + `).ast; + original.findChild(isBlock).statements.push(undefined); + testClone(original); + }); + + it('clones comment statement with undefined comments array', () => { + const original = Parser.parse(` + 'hello world + `).ast; + original.findChild(isCommentStatement).comments = undefined; + testClone(original); + }); + + it('clones class with undefined method modifiers array', () => { + const original = Parser.parse(` + class Movie + sub test() + end sub + end class + `).ast; + original.findChild(isMethodStatement).modifiers = undefined; + testClone(original); + }); + + it('clones class with undefined func', () => { + const original = Parser.parse(` + class Movie + sub test() + end sub + end class + `).ast; + original.findChild(isMethodStatement).func = undefined; + testClone(original); + }); + + it('clones ExpressionStatement', () => { + testClone(` + sub main() + test() + end sub + `); + }); + + it('clones ExpressionStatement without an expression', () => { + const original = Parser.parse(` + sub main() + test() + end sub + `).ast; + original.findChild(isExpressionStatement).expression = undefined; + original.findChild(isFunctionExpression).callExpressions = []; + testClone(original); + }); + + it('clones IfStatement', () => { + testClone(` + sub main() + if true + end if + if true then + end if + if true + print 1 + else if true + print 1 + else + print 1 + end if + end sub + `); + }); + + it('clones IfStatement without condition or branches', () => { + const original = Parser.parse(` + sub main() + if true + end if + end sub + `).ast; + original.findChild(isIfStatement).condition = undefined; + original.findChild(isIfStatement).thenBranch = undefined; + original.findChild(isIfStatement).elseBranch = undefined; + testClone(original); + }); + + it('clones IncrementStatement', () => { + testClone(` + sub main() + i = 0 + i++ + end sub + `); + }); + + it('clones IncrementStatement with missing `value`', () => { + const original = Parser.parse(` + sub main() + i = 0 + i++ + end sub + `).ast; + original.findChild(isIncrementStatement).value = undefined; + testClone(original); + }); + + it('clones PrintStatement with undefined expressions array', () => { + const original = Parser.parse(` + sub main() + print 1 + end sub + `).ast; + original.findChild(isPrintStatement).expressions = undefined; + testClone(original); + }); + + it('clones PrintStatement with undefined expression in the expressions array', () => { + const original = Parser.parse(` + sub main() + print 1 + end sub + `).ast; + original.findChild(isPrintStatement).expressions.push(undefined); + testClone(original); + }); + + it('clones DimStatement', () => { + testClone(` + sub main() + dim alpha[1,2] + end sub + `); + }); + + it('clones DimStatement with undefined dimensions', () => { + const original = Parser.parse(` + sub main() + dim alpha[1,2] + end sub + `).ast; + original.findChild(isDimStatement).dimensions = undefined; + testClone(original); + }); + + it('clones DimStatement with undefined as item in dimensions', () => { + const original = Parser.parse(` + sub main() + dim alpha[1,2] + end sub + `).ast; + original.findChild(isDimStatement).dimensions.push(undefined); + testClone(original); + }); + + it('clones Goto statement', () => { + testClone(` + sub main() + label1: + for i = 0 to 10 + goto label1 + end for + end sub + `); + }); + + it('clones return statement', () => { + testClone(` + sub main() + return + end sub + `); + }); + + it('clones return statement with value', () => { + testClone(` + function test() + return true + end function + `); + }); + + it('clones return statement with undefined value expression', () => { + const original = Parser.parse(` + function test() + return true + end function + `).ast; + original.findChild(isReturnStatement).value = undefined; + testClone(original); + }); + + it('clones stop statement', () => { + testClone(` + sub main() + stop + end sub + `); + }); + + it('clones ForStatement', () => { + testClone(` + function test() + for i = 0 to 10 step 2 + end for + end function + `); + }); + + it('clones ForStatement with undefined items', () => { + const original = Parser.parse(` + function test() + for i = 0 to 10 step 2 + end for + end function + `).ast; + original.findChild(isForStatement).counterDeclaration = undefined; + original.findChild(isForStatement).finalValue = undefined; + original.findChild(isForStatement).body = undefined; + original.findChild(isForStatement).increment = undefined; + testClone(original); + }); + + it('clones ForEachStatement', () => { + testClone(` + function test() + for each item in [1, 2, 3] + end for + end function + `); + }); + + it('clones ForEachStatement with undefined props', () => { + const original = Parser.parse(` + function test() + for each item in [1, 2, 3] + end for + end function + `).ast; + original.findChild(isForEachStatement).target = undefined; + original.findChild(isForEachStatement).body = undefined; + testClone(original); + }); + + it('clones EndStatement', () => { + testClone(` + function test() + end + end function + `); + }); + + it('clones ExitFor statement', () => { + testClone(` + sub main() + for i = 0 to 10 + exit for + end for + end sub + `); + }); + + it('clones While statement', () => { + testClone(` + sub main() + while true + end while + end sub + `); + }); + + it('clones While statement', () => { + testClone(` + sub main() + while true + end while + end sub + `); + }); + + it('clones ExitWhile statement', () => { + testClone(` + sub main() + while true + exit while + end while + end sub + `); + }); + + it('clones tryCatch statement', () => { + testClone(` + sub main() + try + catch e + end try + end sub + `); + }); + + it('clones tryCatch statement when missing branches', () => { + const original = Parser.parse(` + sub main() + try + print 1 + catch e + print 2 + end try + end sub + `).ast; + original.findChild(isTryCatchStatement).tryBranch = undefined; + original.findChild(isTryCatchStatement).catchStatement = undefined; + testClone(original); + }); + + it('clones tryCatch statement when missing catch branch', () => { + const original = Parser.parse(` + sub main() + try + print 1 + catch e + print 2 + end try + end sub + `).ast; + original.findChild(isCatchStatement).catchBranch = undefined; + testClone(original); + }); + + it('clones throw statement', () => { + testClone(` + sub main() + throw "Crash" + end sub + `); + }); + + it('clones throw statement with missing expression', () => { + const original = Parser.parse(` + sub main() + throw "Crash" + end sub + `).ast; + original.findChild(isThrowStatement).expression = undefined; + testClone(original); + }); + + it('clones FunctionStatement when missing .func', () => { + const original = Parser.parse(` + sub main() + end sub + `).ast; + original.findChild(isFunctionStatement).func = undefined; + testClone(original); + }); + + it('clones empty enum statement', () => { + testClone(` + enum Direction + end enum + `); + }); + + it('clones enum statement with comments', () => { + testClone(` + enum Direction + 'the up direction + up = "up" + end enum + `); + }); + + it('clones enum statement with missing body', () => { + const original = Parser.parse(` + enum Direction + 'the up direction + up = "up" + end enum + `).ast; + original.findChild(isEnumStatement).body = undefined; + testClone(original); + }); + + it('clones enum statement with undefined in body', () => { + const original = Parser.parse(` + enum Direction + 'the up direction + up = "up" + end enum + `).ast; + original.findChild(isEnumStatement).body.push(undefined); + testClone(original); + }); + + it('clones enum member with missing value', () => { + const original = Parser.parse(` + enum Direction + up = "up" + end enum + `).ast; + original.findChild(isEnumMemberStatement).value = undefined; + testClone(original); + }); + + it('clones const', () => { + const original = Parser.parse(` + const key = "KEY" + `).ast; + testClone(original); + }); + + + it('clones const with missing value', () => { + const original = Parser.parse(` + const key = "KEY" + `).ast; + original.findChild(isConstStatement).value = undefined; + + testClone(original); + }); + + it('clones continue statement', () => { + testClone(` + sub main() + for i = 0 to 10 + continue for + end for + end sub + `); + }); + + it('clones WhileStatement', () => { + const original = Parser.parse(` + sub main() + while true + print hello + end while + end sub + `).ast; + original.findChild>(isWhileStatement).condition = undefined; + original.findChild>(isWhileStatement).body = undefined; + + testClone(original); + }); + + it('clones DottedSetStatement', () => { + const original = Parser.parse(` + sub main() + m.value = true + end sub + `).ast; + + testClone(original); + }); + + it('clones DottedSetStatement with missing properties', () => { + const original = Parser.parse(` + sub main() + m.value = true + end sub + `).ast; + original.findChild>(isDottedSetStatement).obj = undefined; + original.findChild>(isDottedSetStatement).value = undefined; + + testClone(original); + }); + + it('clones IndexedSetStatement with missing props', () => { + const original = Parser.parse(` + sub main() + m["value"] = true + end sub + `).ast; + original.findChild>(isIndexedSetStatement).obj = undefined; + original.findChild>(isIndexedSetStatement).value = undefined; + + testClone(original); + }); + + it('clones IndexedSetStatement', () => { + const original = Parser.parse(` + sub main() + m["value"] = true + end sub + `).ast; + + testClone(original); + }); + + it('clones IndexedSetStatement', () => { + const original = Parser.parse(` + sub main() + m["value"][2] = true + m["value", 2] = true + end sub + `).ast; + + testClone(original); + }); + + it('clones IndexedSetStatement with undefined additional index', () => { + const original = Parser.parse(` + sub main() + m["value", 2] = true + end sub + `).ast; + original.findChild>(isIndexedSetStatement).additionalIndexes[0] = undefined; + + testClone(original); + }); + + it('clones IndexedSetStatement with missing props', () => { + const original = Parser.parse(` + sub main() + m["value"] = true + end sub + `).ast; + original.findChild>(isIndexedSetStatement).index = undefined; + original.findChild>(isIndexedSetStatement).additionalIndexes = undefined; + + testClone(original); + }); + + it('clones LibraryStatement', () => { + const original = Parser.parse(` + Library "v30/bslCore.brs" + `).ast; + + testClone(original); + }); + + it('clones LibraryStatement with missing tokens', () => { + const original = Parser.parse(` + Library "v30/bslCore.brs" + `).ast; + original.findChild>(isLibraryStatement).tokens = undefined; + + testClone(original); + }); + + it('clones NamespaceStatement', () => { + const original = Parser.parse(` + namespace Alpha + end namespace + `).ast; + + testClone(original); + }); + + it('clones NamespaceStatement with missing items', () => { + const original = Parser.parse(` + namespace Alpha + end namespace + `).ast; + original.findChild>(isNamespaceStatement).nameExpression = undefined; + original.findChild>(isNamespaceStatement).body = undefined; + + testClone(original); + }); + + it('clones ImportStatement', () => { + const original = Parser.parse(` + import "Something.brs" + `).ast; + + testClone(original); + }); + + it('clones BinaryExpression', () => { + const original = Parser.parse(` + sub test() + print 1 + 2 + end sub + `).ast; + + testClone(original); + }); + + it('clones BinaryExpression with missing props', () => { + const original = Parser.parse(` + sub test() + print 1 + 2 + end sub + `).ast; + original.findChild>(isBinaryExpression).left = undefined; + original.findChild>(isBinaryExpression).right = undefined; + + testClone(original); + }); + + it('clones CallExpression', () => { + const original = Parser.parse(` + sub test() + test() + end sub + `).ast; + + testClone(original); + }); + + it('clones CallExpression with args', () => { + const original = Parser.parse(` + sub test() + test(1,2,3) + end sub + `).ast; + + testClone(original); + }); + + it('clones CallExpression with missing props', () => { + const original = Parser.parse(` + sub test() + test(1,2,3) + end sub + `).ast; + original.findChild>(isCallExpression).callee = undefined; + original.findChild>(isCallExpression).args = undefined; + + testClone(original); + }); + + it('clones CallExpression with args containing undefined', () => { + const original = Parser.parse(` + sub test() + test(1,2,3) + end sub + `).ast; + original.findChild>(isCallExpression).args[0] = undefined; + + testClone(original); + }); + + it('clones FunctionExpression', () => { + const original = Parser.parse(` + sub test() + end sub + `).ast; + + testClone(original); + }); + + it('clones FunctionExpression with undefined props', () => { + const original = Parser.parse(` + sub test() + end sub + `).ast; + original.findChild>(isFunctionExpression).parameters = undefined; + original.findChild>(isFunctionExpression).body = undefined; + + testClone(original); + }); + + it('clones FunctionExpression with a parameter that is undefined', () => { + const original = Parser.parse(` + sub test(p1) + end sub + `).ast; + original.findChild>(isFunctionExpression).parameters[0] = undefined; + + testClone(original); + }); + + it('clones FunctionParameterExpression', () => { + const original = Parser.parse(` + sub test(p1) + end sub + `).ast; + + testClone(original); + }); + + it('clones FunctionParameterExpression with default value', () => { + const original = Parser.parse(` + sub test(p1 = true) + end sub + `).ast; + + testClone(original); + }); + + + it('clones FunctionParameterExpression with undefined default value', () => { + const original = Parser.parse(` + sub test(p1 = true) + end sub + `).ast; + original.findChild>(isFunctionExpression).parameters[0].defaultValue = undefined; + + testClone(original); + }); + + it('clones NamespacedVariableNameExpression', () => { + const original = Parser.parse(` + sub test(p1 as Alpha.Beta) + end sub + `).ast; + + testClone(original); + }); + + it('clones NamespacedVariableNameExpression with undefined expression', () => { + const original = Parser.parse(` + class Person extends Alpha.Humanoid + end class + `).ast; + original.findChild>(isClassStatement).parentClassName.expression = undefined; + + testClone(original); + }); + + it('clones DottedGetExpression', () => { + const original = Parser.parse(` + sub test() + print alpha.beta.charlie + end sub + `).ast; + + testClone(original); + }); + + it('clones DottedGetExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print alpha.beta.charlie + end sub + `).ast; + original.findChild>(isDottedGetExpression).obj = undefined; + + testClone(original); + }); + + it('clones XmlAttributeGetExpression', () => { + const original = Parser.parse(` + sub test() + print xml@name + end sub + `).ast; + + testClone(original); + }); + + it('clones XmlAttributeGetExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print xml@name + end sub + `).ast; + original.findChild>(isXmlAttributeGetExpression).obj = undefined; + + testClone(original); + }); + + it('clones IndexedGetExpression', () => { + const original = Parser.parse(` + sub test() + print m.stuff[0] + end sub + `).ast; + + testClone(original); + }); + + it('clones IndexedGetExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print m.stuff[0] + end sub + `).ast; + original.findChild>(isIndexedGetExpression).obj = undefined; + original.findChild>(isIndexedGetExpression).index = undefined; + original.findChild>(isIndexedGetExpression).additionalIndexes = undefined; + + testClone(original); + }); + + it('clones IndexedGetExpression with additionalIndexes', () => { + const original = Parser.parse(` + sub test() + print m.stuff[0, 1] + end sub + `).ast; + + testClone(original); + }); + + it('clones IndexedGetExpression with additionalIndexes having undefined', () => { + const original = Parser.parse(` + sub test() + print m.stuff[0, 1] + end sub + `).ast; + original.findChild>(isIndexedGetExpression).additionalIndexes[0] = undefined; + + testClone(original); + }); + + it('clones GroupingExpression', () => { + const original = Parser.parse(` + sub test() + print (1 + 2) + end sub + `).ast; + + testClone(original); + }); + + it('clones GroupingExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print (1 + 2) + end sub + `).ast; + original.findChild>(isGroupingExpression).expression = undefined; + + testClone(original); + }); + + it('clones LiteralExpression', () => { + const original = Parser.parse(` + sub test() + print true + end sub + `).ast; + + testClone(original); + }); + + it('clones ExcapedCharCodeLiteralExpression', () => { + const original = Parser.parse(` + sub test() + print \`\n\` + end sub + `).ast; + + testClone(original); + }); + + it('clones ArrayLiteralExpression', () => { + const original = Parser.parse(` + sub test() + print [] + end sub + `).ast; + + testClone(original); + }); + + it('clones ArrayLiteralExpression with undefined items', () => { + const original = Parser.parse(` + sub test() + print [] + end sub + `).ast; + original.findChild>(isArrayLiteralExpression).elements = undefined; + + testClone(original); + }); + + it('clones ArrayLiteralExpression with with elements having an undefined', () => { + const original = Parser.parse(` + sub test() + print [1,2,3] + end sub + `).ast; + original.findChild>(isArrayLiteralExpression).elements[0] = undefined; + + testClone(original); + }); + + it('clones AAMemberExpression', () => { + const original = Parser.parse(` + sub test() + movie = { + duration: 20 + } + end sub + `).ast; + + testClone(original); + }); + + it('clones AAMemberExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + movie = { + duration: 20 + } + end sub + `).ast; + original.findChild>(isAAMemberExpression).value = undefined; + + testClone(original); + }); + + it('clones AALiteralExpression', () => { + const original = Parser.parse(` + sub test() + movie = { + duration: 20 + } + end sub + `).ast; + + testClone(original); + }); + + it('clones AALiteralExpression with undefined items', () => { + const original = Parser.parse(` + sub test() + movie = { + duration: 20 + } + end sub + `).ast; + original.findChild>(isAALiteralExpression).elements = undefined; + + testClone(original); + }); + + it('clones AALiteralExpression with undefined items', () => { + const original = Parser.parse(` + sub test() + movie = { + duration: 20 + } + end sub + `).ast; + original.findChild(isAALiteralExpression).elements.push(undefined); + + testClone(original); + }); + + it('clones UnaryExpression', () => { + const original = Parser.parse(` + sub test() + print not true + end sub + `).ast; + + testClone(original); + }); + + it('clones UnaryExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print not true + end sub + `).ast; + original.findChild>(isUnaryExpression).right = undefined; + + testClone(original); + }); + + it('clones SourceLiteralExpression', () => { + const original = Parser.parse(` + sub test() + print LINE_NUM + end sub + `).ast; + + testClone(original); + }); + + it('clones NewExpression', () => { + const original = Parser.parse(` + sub test() + print new Person() + end sub + `).ast; + + testClone(original); + }); + + it('clones NewExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print new Person() + end sub + `).ast; + original.findChild>(isNewExpression).call = undefined; + + testClone(original); + }); + + it('clones CallfuncExpression', () => { + const original = Parser.parse(` + sub test() + print node@.run(1) + end sub + `).ast; + + testClone(original); + }); + + it('clones CallfuncExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print node@.run() + end sub + `).ast; + original.findChild>(isCallfuncExpression).callee = undefined; + original.findChild>(isCallfuncExpression).args = undefined; + + testClone(original); + }); + + it('clones CallfuncExpression with undefined args', () => { + const original = Parser.parse(` + sub test() + print node@.run() + end sub + `).ast; + original.findChild>(isCallfuncExpression).args[0] = undefined; + + testClone(original); + }); + + it('clones TemplateStringQuasiExpression', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name}\` + end sub + `).ast; + + testClone(original); + }); + + it('clones TemplateStringQuasiExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name}\` + end sub + `).ast; + original.findChild>(isTemplateStringQuasiExpression).expressions = undefined; + + testClone(original); + }); + + it('clones TemplateStringQuasiExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name}\` + end sub + `).ast; + original.findChild>(isTemplateStringQuasiExpression).expressions[0] = undefined; + + testClone(original); + }); + + it('clones TemplateStringExpression', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name} \\n\` + end sub + `).ast; + + testClone(original); + }); + + it('clones TemplateStringExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name}\` + end sub + `).ast; + original.findChild>(isTemplateStringExpression).quasis = undefined; + original.findChild>(isTemplateStringExpression).expressions = undefined; + + testClone(original); + }); + + it('clones TemplateStringExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print \`hello \${name}\` + end sub + `).ast; + original.findChild>(isTemplateStringExpression).quasis.push(undefined); + original.findChild>(isTemplateStringExpression).expressions.push(undefined); + + testClone(original); + }); + + it('clones TemplateStringExpression', () => { + const original = Parser.parse(` + sub test() + print tag\`hello \${name} \\n\` + end sub + `).ast; + + testClone(original); + }); + + it('clones TemplateStringExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print tag\`hello \${name}\` + end sub + `).ast; + original.findChild>(isTaggedTemplateStringExpression).quasis = undefined; + original.findChild>(isTaggedTemplateStringExpression).expressions = undefined; + + testClone(original); + }); + + it('clones TemplateStringExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print tag\`hello \${name}\` + end sub + `).ast; + original.findChild>(isTaggedTemplateStringExpression).quasis.push(undefined); + original.findChild>(isTaggedTemplateStringExpression).expressions.push(undefined); + + testClone(original); + }); + + it('clones TernaryExpression', () => { + const original = Parser.parse(` + sub test() + print true ? 1 : 2 + end sub + `).ast; + + testClone(original); + }); + + it('clones TernaryExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print true ? 1 : 2 + end sub + `).ast; + original.findChild>(isTernaryExpression).test = undefined; + original.findChild>(isTernaryExpression).consequent = undefined; + original.findChild>(isTernaryExpression).alternate = undefined; + + testClone(original); + }); + + it('clones NullCoalescingExpression', () => { + const original = Parser.parse(` + sub test() + print a ?? b + end sub + `).ast; + + testClone(original); + }); + + it('clones NullCoalescingExpression with undefined expressions', () => { + const original = Parser.parse(` + sub test() + print a ?? b + end sub + `).ast; + original.findChild>(isNullCoalescingExpression).consequent = undefined; + original.findChild>(isNullCoalescingExpression).alternate = undefined; + + testClone(original); + }); + + it('clones RegexLiteralExpression', () => { + const original = Parser.parse(` + sub test() + print /test/gi + end sub + `).ast; + + testClone(original); + }); + + it('clones TypeCastExpression', () => { + const original = Parser.parse(` + sub test() + print name as string + end sub + `).ast; + + testClone(original); + }); + + it('clones TypeCastExpression with undefined expression', () => { + const original = Parser.parse(` + sub test() + print name as string + end sub + `).ast; + original.findChild>(isTypeCastExpression).obj = undefined; + + testClone(original); + }); + + it('clones AnnotationExpressions above every statement type', () => { + const original = Parser.parse(` + @annotation() + sub test() + @annotation() + statement = true + @annotation() + call() + @annotation() + 'comment + end sub + + @annotation() + class Person + end class + + @annotation() + enum Direction + end enum + + @annotation() + namespace alpha + end namespace + + @annotation() + const thing = 1 + `).ast; + + testClone(original); + }); + }); }); diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index affa32b33..a4e00f0ba 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -83,7 +83,7 @@ export abstract class AstNode { * Find the first child where the matcher evaluates to true. * @param matcher a function called for each node. If you return true, this function returns the specified node. If you return a node, that node is returned. all other return values continue the loop */ - public findChild(matcher: (node: AstNode, cancellationSource) => boolean | AstNode | undefined | void, options?: WalkOptions): TNode | undefined { + public findChild(matcher: (node: AstNode, cancellationSource) => boolean | AstNode | undefined | void, options?: WalkOptions): TNode | undefined { const cancel = new CancellationTokenSource(); let result: AstNode | undefined; this.walk((node) => { @@ -97,7 +97,7 @@ export abstract class AstNode { ...options ?? {}, cancel: cancel.token }); - return result as TNode; + return result as unknown as TNode; } /** @@ -121,6 +121,36 @@ export abstract class AstNode { walkMode: WalkMode.visitAllRecursive }); } + + /** + * Clone this node and all of its children. This creates a completely detached and identical copy of the AST. + * All tokens, statements, expressions, range, and location are cloned. + */ + public abstract clone(); + + /** + * Helper function for creating a clone. This will clone any attached annotations, as well as reparent the cloned node's children to the clone + */ + protected finalizeClone( + clone: T, + propsToReparent?: Array<{ [K in keyof T]: T[K] extends AstNode | AstNode[] ? K : never }[keyof T]> + ) { + //clone the annotations if they exist + if (Array.isArray((this as unknown as Statement).annotations)) { + (clone as unknown as Statement).annotations = (this as unknown as Statement).annotations?.map(x => x.clone()); + } + //reparent all of the supplied props + for (let key of propsToReparent ?? []) { + const children = (Array.isArray(clone?.[key]) ? clone[key] : [clone?.[key]]) as any[]; + for (let child of children ?? []) { + if (child) { + (clone[key as any] as AstNode).parent = clone; + } + } + } + return clone; + } + } export abstract class Statement extends AstNode { diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 50b4f1884..1eb3c706b 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -10,7 +10,7 @@ import * as fileUrl from 'file-url'; import type { WalkOptions, WalkVisitor } from '../astUtils/visitors'; import { createVisitor, WalkMode } from '../astUtils/visitors'; import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors'; -import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isStringType, isTemplateStringExpression, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; +import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isStringType, isTemplateStringExpression, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; import type { TranspileResult, TypedefProvider } from '../interfaces'; import { VoidType } from '../types/VoidType'; import { DynamicType } from '../types/DynamicType'; @@ -51,6 +51,17 @@ export class BinaryExpression extends Expression { walk(this, 'right', visitor, options); } } + + public clone() { + return this.finalizeClone( + new BinaryExpression( + this.left?.clone(), + util.cloneToken(this.operator), + this.right?.clone() + ), + ['left', 'right'] + ); + } } export class CallExpression extends Expression { @@ -73,7 +84,7 @@ export class CallExpression extends Expression { unused?: any ) { super(); - this.range = util.createBoundingRange(this.callee, this.openingParen, ...args, this.closingParen); + this.range = util.createBoundingRange(this.callee, this.openingParen, ...args ?? [], this.closingParen); } public readonly range: Range | undefined; @@ -121,6 +132,18 @@ export class CallExpression extends Expression { walkArray(this.args, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new CallExpression( + this.callee?.clone(), + util.cloneToken(this.openingParen), + util.cloneToken(this.closingParen), + this.args?.map(e => e?.clone()) + ), + ['callee', 'args'] + ); + } } export class FunctionExpression extends Expression implements TypedefProvider { @@ -205,7 +228,7 @@ export class FunctionExpression extends Expression implements TypedefProvider { public get range() { return util.createBoundingRange( this.functionType, this.leftParen, - ...this.parameters, + ...this.parameters ?? [], this.rightParen, this.asToken, this.returnTypeToken, @@ -322,6 +345,30 @@ export class FunctionExpression extends Expression implements TypedefProvider { } return functionType; } + + public clone() { + const clone = this.finalizeClone( + new FunctionExpression( + this.parameters?.map(e => e?.clone()), + this.body?.clone(), + util.cloneToken(this.functionType), + util.cloneToken(this.end), + util.cloneToken(this.leftParen), + util.cloneToken(this.rightParen), + util.cloneToken(this.asToken), + util.cloneToken(this.returnTypeToken) + ), + ['body'] + ); + + //rebuild the .callExpressions list in the clone + clone.body?.walk?.((node) => { + if (isCallExpression(node) && !isNewExpression(node.parent)) { + clone.callExpressions.push(node); + } + }, { walkMode: WalkMode.visitExpressions }); + return clone; + } } export class FunctionParameterExpression extends Expression { @@ -397,6 +444,18 @@ export class FunctionParameterExpression extends Expression { walk(this, 'defaultValue', visitor, options); } } + + public clone() { + return this.finalizeClone( + new FunctionParameterExpression( + util.cloneToken(this.name), + util.cloneToken(this.typeToken), + this.defaultValue?.clone(), + util.cloneToken(this.asToken) + ), + ['defaultValue'] + ); + } } export class NamespacedVariableNameExpression extends Expression { @@ -405,7 +464,7 @@ export class NamespacedVariableNameExpression extends Expression { readonly expression: DottedGetExpression | VariableExpression ) { super(); - this.range = expression.range; + this.range = expression?.range; } range: Range | undefined; @@ -446,6 +505,14 @@ export class NamespacedVariableNameExpression extends Expression { walk(this, 'expression', visitor, options); } } + + public clone() { + return this.finalizeClone( + new NamespacedVariableNameExpression( + this.expression?.clone() + ) + ); + } } export class DottedGetExpression extends Expression { @@ -482,6 +549,16 @@ export class DottedGetExpression extends Expression { } } + public clone() { + return this.finalizeClone( + new DottedGetExpression( + this.obj?.clone(), + util.cloneToken(this.name), + util.cloneToken(this.dot) + ), + ['obj'] + ); + } } export class XmlAttributeGetExpression extends Expression { @@ -512,6 +589,17 @@ export class XmlAttributeGetExpression extends Expression { walk(this, 'obj', visitor, options); } } + + public clone() { + return this.finalizeClone( + new XmlAttributeGetExpression( + this.obj?.clone(), + util.cloneToken(this.name), + util.cloneToken(this.at) + ), + ['obj'] + ); + } } export class IndexedGetExpression extends Expression { @@ -567,6 +655,20 @@ export class IndexedGetExpression extends Expression { walkArray(this.additionalIndexes, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new IndexedGetExpression( + this.obj?.clone(), + this.index?.clone(), + util.cloneToken(this.openingSquare), + util.cloneToken(this.closingSquare), + util.cloneToken(this.questionDotToken), + this.additionalIndexes?.map(e => e?.clone()) + ), + ['obj', 'index', 'additionalIndexes'] + ); + } } export class GroupingExpression extends Expression { @@ -599,6 +701,19 @@ export class GroupingExpression extends Expression { walk(this, 'expression', visitor, options); } } + + public clone() { + return this.finalizeClone( + new GroupingExpression( + { + left: util.cloneToken(this.tokens.left), + right: util.cloneToken(this.tokens.right) + }, + this.expression?.clone() + ), + ['expression'] + ); + } } export class LiteralExpression extends Expression { @@ -642,6 +757,14 @@ export class LiteralExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new LiteralExpression( + util.cloneToken(this.token) + ) + ); + } } /** @@ -666,6 +789,14 @@ export class EscapedCharCodeLiteralExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new EscapedCharCodeLiteralExpression( + util.cloneToken(this.token) + ) + ); + } } export class ArrayLiteralExpression extends Expression { @@ -676,7 +807,7 @@ export class ArrayLiteralExpression extends Expression { readonly hasSpread = false ) { super(); - this.range = util.createBoundingRange(this.open, ...this.elements, this.close); + this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close); } public readonly range: Range | undefined; @@ -734,6 +865,18 @@ export class ArrayLiteralExpression extends Expression { walkArray(this.elements, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new ArrayLiteralExpression( + this.elements?.map(e => e?.clone()), + util.cloneToken(this.open), + util.cloneToken(this.close), + this.hasSpread + ), + ['elements'] + ); + } } export class AAMemberExpression extends Expression { @@ -759,6 +902,17 @@ export class AAMemberExpression extends Expression { walk(this, 'value', visitor, options); } + public clone() { + return this.finalizeClone( + new AAMemberExpression( + util.cloneToken(this.keyToken), + util.cloneToken(this.colonToken), + this.value?.clone() + ), + ['value'] + ); + } + } export class AALiteralExpression extends Expression { @@ -768,7 +922,7 @@ export class AALiteralExpression extends Expression { readonly close: Token ) { super(); - this.range = util.createBoundingRange(this.open, ...this.elements, this.close); + this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close); } public readonly range: Range | undefined; @@ -848,6 +1002,17 @@ export class AALiteralExpression extends Expression { walkArray(this.elements, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new AALiteralExpression( + this.elements?.map(e => e?.clone()), + util.cloneToken(this.open), + util.cloneToken(this.close) + ), + ['elements'] + ); + } } export class UnaryExpression extends Expression { @@ -881,6 +1046,16 @@ export class UnaryExpression extends Expression { walk(this, 'right', visitor, options); } } + + public clone() { + return this.finalizeClone( + new UnaryExpression( + util.cloneToken(this.operator), + this.right?.clone() + ), + ['right'] + ); + } } export class VariableExpression extends Expression { @@ -922,6 +1097,14 @@ export class VariableExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new VariableExpression( + util.cloneToken(this.name) + ) + ); + } } export class SourceLiteralExpression extends Expression { @@ -1014,6 +1197,14 @@ export class SourceLiteralExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new SourceLiteralExpression( + util.cloneToken(this.token) + ) + ); + } } /** @@ -1057,6 +1248,16 @@ export class NewExpression extends Expression { walk(this, 'call', visitor, options); } } + + public clone() { + return this.finalizeClone( + new NewExpression( + util.cloneToken(this.newKeyword), + this.call?.clone() + ), + ['call'] + ); + } } export class CallfuncExpression extends Expression { @@ -1074,7 +1275,7 @@ export class CallfuncExpression extends Expression { operator, methodName, openingParen, - ...args, + ...args ?? [], closingParen ); } @@ -1125,6 +1326,20 @@ export class CallfuncExpression extends Expression { walkArray(this.args, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new CallfuncExpression( + this.callee?.clone(), + util.cloneToken(this.operator), + util.cloneToken(this.methodName), + util.cloneToken(this.openingParen), + this.args?.map(e => e?.clone()), + util.cloneToken(this.closingParen) + ), + ['callee', 'args'] + ); + } } /** @@ -1137,7 +1352,7 @@ export class TemplateStringQuasiExpression extends Expression { ) { super(); this.range = util.createBoundingRange( - ...expressions + ...expressions ?? [] ); } readonly range: Range | undefined; @@ -1165,6 +1380,15 @@ export class TemplateStringQuasiExpression extends Expression { walkArray(this.expressions, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new TemplateStringQuasiExpression( + this.expressions?.map(e => e?.clone()) + ), + ['expressions'] + ); + } } export class TemplateStringExpression extends Expression { @@ -1177,8 +1401,8 @@ export class TemplateStringExpression extends Expression { super(); this.range = util.createBoundingRange( openingBacktick, - quasis[0], - quasis[quasis.length - 1], + quasis?.[0], + quasis?.[quasis?.length - 1], closingBacktick ); } @@ -1241,7 +1465,7 @@ export class TemplateStringExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { if (options.walkMode & InternalWalkMode.walkExpressions) { //walk the quasis and expressions in left-to-right order - for (let i = 0; i < this.quasis.length; i++) { + for (let i = 0; i < this.quasis?.length; i++) { walk(this.quasis, i, visitor, options, this); //this skips the final loop iteration since we'll always have one more quasi than expression @@ -1251,6 +1475,18 @@ export class TemplateStringExpression extends Expression { } } } + + public clone() { + return this.finalizeClone( + new TemplateStringExpression( + util.cloneToken(this.openingBacktick), + this.quasis?.map(e => e?.clone()), + this.expressions?.map(e => e?.clone()), + util.cloneToken(this.closingBacktick) + ), + ['quasis', 'expressions'] + ); + } } export class TaggedTemplateStringExpression extends Expression { @@ -1265,8 +1501,8 @@ export class TaggedTemplateStringExpression extends Expression { this.range = util.createBoundingRange( tagName, openingBacktick, - quasis[0], - quasis[quasis.length - 1], + quasis?.[0], + quasis?.[quasis?.length - 1], closingBacktick ); } @@ -1318,7 +1554,7 @@ export class TaggedTemplateStringExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { if (options.walkMode & InternalWalkMode.walkExpressions) { //walk the quasis and expressions in left-to-right order - for (let i = 0; i < this.quasis.length; i++) { + for (let i = 0; i < this.quasis?.length; i++) { walk(this.quasis, i, visitor, options, this); //this skips the final loop iteration since we'll always have one more quasi than expression @@ -1328,6 +1564,19 @@ export class TaggedTemplateStringExpression extends Expression { } } } + + public clone() { + return this.finalizeClone( + new TaggedTemplateStringExpression( + util.cloneToken(this.tagName), + util.cloneToken(this.openingBacktick), + this.quasis?.map(e => e?.clone()), + this.expressions?.map(e => e?.clone()), + util.cloneToken(this.closingBacktick) + ), + ['quasis', 'expressions'] + ); + } } export class AnnotationExpression extends Expression { @@ -1375,6 +1624,16 @@ export class AnnotationExpression extends Expression { ...(this.call?.transpile(state) ?? []) ]; } + + public clone() { + const clone = this.finalizeClone( + new AnnotationExpression( + util.cloneToken(this.atToken), + util.cloneToken(this.nameToken) + ) + ); + return clone; + } } export class TernaryExpression extends Expression { @@ -1464,6 +1723,19 @@ export class TernaryExpression extends Expression { walk(this, 'alternate', visitor, options); } } + + public clone() { + return this.finalizeClone( + new TernaryExpression( + this.test?.clone(), + util.cloneToken(this.questionMarkToken), + this.consequent?.clone(), + util.cloneToken(this.colonToken), + this.alternate?.clone() + ), + ['test', 'consequent', 'alternate'] + ); + } } export class NullCoalescingExpression extends Expression { @@ -1547,6 +1819,17 @@ export class NullCoalescingExpression extends Expression { walk(this, 'alternate', visitor, options); } } + + public clone() { + return this.finalizeClone( + new NullCoalescingExpression( + this.consequent?.clone(), + util.cloneToken(this.questionQuestionToken), + this.alternate?.clone() + ), + ['consequent', 'alternate'] + ); + } } export class RegexLiteralExpression extends Expression { @@ -1590,6 +1873,14 @@ export class RegexLiteralExpression extends Expression { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new RegexLiteralExpression({ + regexLiteral: util.cloneToken(this.tokens.regexLiteral) + }) + ); + } } @@ -1617,6 +1908,17 @@ export class TypeCastExpression extends Expression { walk(this, 'obj', visitor, options); } } + + public clone() { + return this.finalizeClone( + new TypeCastExpression( + this.obj?.clone(), + util.cloneToken(this.asToken), + util.cloneToken(this.typeToken) + ), + ['obj'] + ); + } } // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts index a9812004f..0c00bdc56 100644 --- a/src/parser/Statement.spec.ts +++ b/src/parser/Statement.spec.ts @@ -124,5 +124,4 @@ describe('Statement', () => { }); }); }); - }); diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 173774b97..9cd321c7b 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -35,6 +35,14 @@ export class EmptyStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new EmptyStatement( + util.cloneRange(this.range) + ) + ); + } } /** @@ -109,6 +117,15 @@ export class Body extends Statement implements TypedefProvider { walkArray(this.statements, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new Body( + this.statements?.map(s => s?.clone()) + ), + ['statements'] + ); + } } export class AssignmentStatement extends Statement { @@ -151,6 +168,17 @@ export class AssignmentStatement extends Statement { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new AssignmentStatement( + util.cloneToken(this.equals), + util.cloneToken(this.name), + this.value?.clone() + ), + ['value'] + ); + } } export class Block extends Statement { @@ -205,6 +233,16 @@ export class Block extends Statement { walkArray(this.statements, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new Block( + this.statements?.map(s => s?.clone()), + util.cloneRange(this.startingRange) + ), + ['statements'] + ); + } } export class ExpressionStatement extends Statement { @@ -212,7 +250,7 @@ export class ExpressionStatement extends Statement { readonly expression: Expression ) { super(); - this.range = this.expression.range; + this.range = this.expression?.range; } public readonly range: Range | undefined; @@ -226,6 +264,15 @@ export class ExpressionStatement extends Statement { walk(this, 'expression', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ExpressionStatement( + this.expression?.clone() + ), + ['expression'] + ); + } } export class CommentStatement extends Statement implements Expression, TypedefProvider { @@ -272,6 +319,15 @@ export class CommentStatement extends Statement implements Expression, TypedefPr walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new CommentStatement( + this.comments?.map(x => util.cloneToken(x)) + ), + ['comments' as any] + ); + } } export class ExitForStatement extends Statement { @@ -296,6 +352,13 @@ export class ExitForStatement extends Statement { //nothing to walk } + public clone() { + return this.finalizeClone( + new ExitForStatement({ + exitFor: util.cloneToken(this.tokens.exitFor) + }) + ); + } } export class ExitWhileStatement extends Statement { @@ -319,6 +382,14 @@ export class ExitWhileStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new ExitWhileStatement({ + exitWhile: util.cloneToken(this.tokens.exitWhile) + }) + ); + } } export class FunctionStatement extends Statement implements TypedefProvider { @@ -327,7 +398,7 @@ export class FunctionStatement extends Statement implements TypedefProvider { public func: FunctionExpression ) { super(); - this.range = this.func.range; + this.range = this.func?.range; } public readonly range: Range | undefined; @@ -385,6 +456,16 @@ export class FunctionStatement extends Statement implements TypedefProvider { walk(this, 'func', visitor, options); } } + + public clone() { + return this.finalizeClone( + new FunctionStatement( + util.cloneToken(this.name), + this.func?.clone() + ), + ['func'] + ); + } } export class IfStatement extends Statement { @@ -500,6 +581,24 @@ export class IfStatement extends Statement { walk(this, 'elseBranch', visitor, options); } } + + public clone() { + return this.finalizeClone( + new IfStatement( + { + if: util.cloneToken(this.tokens.if), + else: util.cloneToken(this.tokens.else), + endIf: util.cloneToken(this.tokens.endIf), + then: util.cloneToken(this.tokens.then) + }, + this.condition?.clone(), + this.thenBranch?.clone(), + this.elseBranch?.clone(), + this.isInline + ), + ['condition', 'thenBranch', 'elseBranch'] + ); + } } export class IncrementStatement extends Statement { @@ -528,6 +627,16 @@ export class IncrementStatement extends Statement { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new IncrementStatement( + this.value?.clone(), + util.cloneToken(this.operator) + ), + ['value'] + ); + } } /** Used to indent the current `print` position to the next 16-character-width output zone. */ @@ -593,6 +702,24 @@ export class PrintStatement extends Statement { walkArray(this.expressions, visitor, options, this, (item) => isExpression(item as any)); } } + + public clone() { + return this.finalizeClone( + new PrintStatement( + { + print: util.cloneToken(this.tokens.print) + }, + this.expressions?.map(e => { + if (isExpression(e as any)) { + return (e as Expression).clone(); + } else { + return util.cloneToken(e as Token); + } + }) + ), + ['expressions' as any] + ); + } } export class DimStatement extends Statement { @@ -639,6 +766,19 @@ export class DimStatement extends Statement { } } + + public clone() { + return this.finalizeClone( + new DimStatement( + util.cloneToken(this.dimToken), + util.cloneToken(this.identifier), + util.cloneToken(this.openingSquare), + this.dimensions?.map(e => e?.clone()), + util.cloneToken(this.closingSquare) + ), + ['dimensions'] + ); + } } export class GotoStatement extends Statement { @@ -668,6 +808,15 @@ export class GotoStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new GotoStatement({ + goto: util.cloneToken(this.tokens.goto), + label: util.cloneToken(this.tokens.label) + }) + ); + } } export class LabelStatement extends Statement { @@ -697,6 +846,15 @@ export class LabelStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new LabelStatement({ + identifier: util.cloneToken(this.tokens.identifier), + colon: util.cloneToken(this.tokens.colon) + }) + ); + } } export class ReturnStatement extends Statement { @@ -732,6 +890,18 @@ export class ReturnStatement extends Statement { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ReturnStatement( + { + return: util.cloneToken(this.tokens.return) + }, + this.value?.clone() + ), + ['value'] + ); + } } export class EndStatement extends Statement { @@ -755,6 +925,14 @@ export class EndStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new EndStatement({ + end: util.cloneToken(this.tokens.end) + }) + ); + } } export class StopStatement extends Statement { @@ -778,6 +956,14 @@ export class StopStatement extends Statement { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new StopStatement({ + stop: util.cloneToken(this.tokens.stop) + }) + ); + } } export class ForStatement extends Statement { @@ -863,6 +1049,22 @@ export class ForStatement extends Statement { walk(this, 'body', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ForStatement( + util.cloneToken(this.forToken), + this.counterDeclaration?.clone(), + util.cloneToken(this.toToken), + this.finalValue?.clone(), + this.body?.clone(), + util.cloneToken(this.endForToken), + util.cloneToken(this.stepToken), + this.increment?.clone() + ), + ['counterDeclaration', 'finalValue', 'body', 'increment'] + ); + } } export class ForEachStatement extends Statement { @@ -932,6 +1134,22 @@ export class ForEachStatement extends Statement { walk(this, 'body', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ForEachStatement( + { + forEach: util.cloneToken(this.tokens.forEach), + in: util.cloneToken(this.tokens.in), + endFor: util.cloneToken(this.tokens.endFor) + }, + util.cloneToken(this.item), + this.target?.clone(), + this.body?.clone() + ), + ['target', 'body'] + ); + } } export class WhileStatement extends Statement { @@ -990,6 +1208,20 @@ export class WhileStatement extends Statement { walk(this, 'body', visitor, options); } } + + public clone() { + return this.finalizeClone( + new WhileStatement( + { + while: util.cloneToken(this.tokens.while), + endWhile: util.cloneToken(this.tokens.endWhile) + }, + this.condition?.clone(), + this.body?.clone() + ), + ['condition', 'body'] + ); + } } export class DottedSetStatement extends Statement { @@ -1034,6 +1266,18 @@ export class DottedSetStatement extends Statement { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new DottedSetStatement( + this.obj?.clone(), + util.cloneToken(this.name), + this.value?.clone(), + util.cloneToken(this.dot) + ), + ['obj', 'value'] + ); + } } export class IndexedSetStatement extends Statement { @@ -1098,6 +1342,20 @@ export class IndexedSetStatement extends Statement { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new IndexedSetStatement( + this.obj?.clone(), + this.index?.clone(), + this.value?.clone(), + util.cloneToken(this.openingSquare), + util.cloneToken(this.closingSquare), + this.additionalIndexes?.map(e => e?.clone()) + ), + ['obj', 'index', 'value', 'additionalIndexes'] + ); + } } export class LibraryStatement extends Statement implements TypedefProvider { @@ -1109,8 +1367,8 @@ export class LibraryStatement extends Statement implements TypedefProvider { ) { super(); this.range = util.createBoundingRange( - this.tokens.library, - this.tokens.filePath + this.tokens?.library, + this.tokens?.filePath ); } @@ -1138,6 +1396,17 @@ export class LibraryStatement extends Statement implements TypedefProvider { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new LibraryStatement( + this.tokens === undefined ? undefined : { + library: util.cloneToken(this.tokens?.library), + filePath: util.cloneToken(this.tokens?.filePath) + } + ) + ); + } } export class NamespaceStatement extends Statement implements TypedefProvider { @@ -1149,14 +1418,15 @@ export class NamespaceStatement extends Statement implements TypedefProvider { public endKeyword: Token ) { super(); - this.name = this.getName(ParseMode.BrighterScript); this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable()); } /** * The string name for this namespace */ - public name: string; + public get name(): string { + return this.getName(ParseMode.BrighterScript); + } public get range() { return this.cacheRange(); @@ -1177,7 +1447,10 @@ export class NamespaceStatement extends Statement implements TypedefProvider { public getName(parseMode: ParseMode) { const parentNamespace = this.findAncestor(isNamespaceStatement); - let name = this.nameExpression.getName(parseMode); + let name = this.nameExpression?.getName?.(parseMode); + if (!name) { + return name; + } if (parentNamespace) { const sep = parseMode === ParseMode.BrighterScript ? '.' : '_'; @@ -1220,6 +1493,20 @@ export class NamespaceStatement extends Statement implements TypedefProvider { walk(this, 'body', visitor, options); } } + + public clone() { + const clone = this.finalizeClone( + new NamespaceStatement( + util.cloneToken(this.keyword), + this.nameExpression?.clone(), + this.body?.clone(), + util.cloneToken(this.endKeyword) + ), + ['nameExpression', 'body'] + ); + clone.cacheRange(); + return clone; + } } export class ImportStatement extends Statement implements TypedefProvider { @@ -1275,6 +1562,15 @@ export class ImportStatement extends Statement implements TypedefProvider { walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new ImportStatement( + util.cloneToken(this.importToken), + util.cloneToken(this.filePathToken) + ) + ); + } } export class InterfaceStatement extends Statement implements TypedefProvider { @@ -1296,7 +1592,7 @@ export class InterfaceStatement extends Statement implements TypedefProvider { this.tokens.name, this.tokens.extends, this.parentInterfaceName, - ...this.body, + ...this.body ?? [], this.tokens.endInterface ); } @@ -1431,6 +1727,20 @@ export class InterfaceStatement extends Statement implements TypedefProvider { walkArray(this.body, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new InterfaceStatement( + util.cloneToken(this.tokens.interface), + util.cloneToken(this.tokens.name), + util.cloneToken(this.tokens.extends), + this.parentInterfaceName?.clone(), + this.body?.map(x => x?.clone()), + util.cloneToken(this.tokens.endInterface) + ), + ['parentInterfaceName', 'body'] + ); + } } export class InterfaceFieldStatement extends Statement implements TypedefProvider { @@ -1505,6 +1815,18 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide return result; } + public clone() { + return this.finalizeClone( + new InterfaceFieldStatement( + util.cloneToken(this.tokens.name), + util.cloneToken(this.tokens.as), + util.cloneToken(this.tokens.type), + this.type?.clone(), + util.cloneToken(this.tokens.optional) + ) + ); + } + } export class InterfaceMethodStatement extends Statement implements TypedefProvider { @@ -1613,6 +1935,23 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid } return result; } + + public clone() { + return this.finalizeClone( + new InterfaceMethodStatement( + util.cloneToken(this.tokens.functionType), + util.cloneToken(this.tokens.name), + util.cloneToken(this.tokens.leftParen), + this.params?.map(p => p?.clone()), + util.cloneToken(this.tokens.rightParen), + util.cloneToken(this.tokens.as), + util.cloneToken(this.tokens.returnType), + this.returnType?.clone(), + util.cloneToken(this.tokens.optional) + ), + ['params'] + ); + } } export class ClassStatement extends Statement implements TypedefProvider { @@ -2003,6 +2342,20 @@ export class ClassStatement extends Statement implements TypedefProvider { walkArray(this.body, visitor, options, this); } } + + public clone() { + return this.finalizeClone( + new ClassStatement( + util.cloneToken(this.classKeyword), + util.cloneToken(this.name), + this.body?.map(x => x?.clone()), + util.cloneToken(this.end), + util.cloneToken(this.extendsKeyword), + this.parentClassName?.clone() + ), + ['body', 'parentClassName'] + ); + } } const accessModifiers = [ @@ -2193,6 +2546,18 @@ export class MethodStatement extends FunctionStatement { walk(this, 'func', visitor, options); } } + + public clone() { + return this.finalizeClone( + new MethodStatement( + this.modifiers?.map(m => util.cloneToken(m)), + util.cloneToken(this.name), + this.func?.clone(), + util.cloneToken(this.override) + ), + ['func'] + ); + } } /** * @deprecated use `MethodStatement` @@ -2281,7 +2646,23 @@ export class FieldStatement extends Statement implements TypedefProvider { walk(this, 'initialValue', visitor, options); } } + + public clone() { + return this.finalizeClone( + new FieldStatement( + util.cloneToken(this.accessModifier), + util.cloneToken(this.name), + util.cloneToken(this.as), + util.cloneToken(this.type), + util.cloneToken(this.equal), + this.initialValue?.clone(), + util.cloneToken(this.optional) + ), + ['initialValue'] + ); + } } + /** * @deprecated use `FieldStatement` */ @@ -2333,6 +2714,20 @@ export class TryCatchStatement extends Statement { walk(this, 'catchStatement', visitor, options); } } + + public clone() { + return this.finalizeClone( + new TryCatchStatement( + { + try: util.cloneToken(this.tokens.try), + endTry: util.cloneToken(this.tokens.endTry) + }, + this.tryBranch?.clone(), + this.catchStatement?.clone() + ), + ['tryBranch', 'catchStatement'] + ); + } } export class CatchStatement extends Statement { @@ -2367,6 +2762,19 @@ export class CatchStatement extends Statement { walk(this, 'catchBranch', visitor, options); } } + + public clone() { + return this.finalizeClone( + new CatchStatement( + { + catch: util.cloneToken(this.tokens.catch) + }, + util.cloneToken(this.exceptionVariable), + this.catchBranch?.clone() + ), + ['catchBranch'] + ); + } } export class ThrowStatement extends Statement { @@ -2406,6 +2814,16 @@ export class ThrowStatement extends Statement { walk(this, 'expression', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ThrowStatement( + util.cloneToken(this.throwToken), + this.expression?.clone() + ), + ['expression'] + ); + } } @@ -2561,6 +2979,20 @@ export class EnumStatement extends Statement implements TypedefProvider { } } + + public clone() { + return this.finalizeClone( + new EnumStatement( + { + enum: util.cloneToken(this.tokens.enum), + name: util.cloneToken(this.tokens.name), + endEnum: util.cloneToken(this.tokens.endEnum) + }, + this.body?.map(x => x?.clone()) + ), + ['body'] + ); + } } export class EnumMemberStatement extends Statement implements TypedefProvider { @@ -2621,6 +3053,19 @@ export class EnumMemberStatement extends Statement implements TypedefProvider { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new EnumMemberStatement( + { + name: util.cloneToken(this.tokens.name), + equal: util.cloneToken(this.tokens.equal) + }, + this.value?.clone() + ), + ['value'] + ); + } } export class ConstStatement extends Statement implements TypedefProvider { @@ -2684,6 +3129,20 @@ export class ConstStatement extends Statement implements TypedefProvider { walk(this, 'value', visitor, options); } } + + public clone() { + return this.finalizeClone( + new ConstStatement( + { + const: util.cloneToken(this.tokens.const), + name: util.cloneToken(this.tokens.name), + equals: util.cloneToken(this.tokens.equals) + }, + this.value?.clone() + ), + ['value'] + ); + } } export class ContinueStatement extends Statement { @@ -2709,7 +3168,17 @@ export class ContinueStatement extends Statement { state.sourceNode(this.tokens.continue, this.tokens.loopType?.text) ]; } + walk(visitor: WalkVisitor, options: WalkOptions) { //nothing to walk } + + public clone() { + return this.finalizeClone( + new ContinueStatement({ + continue: util.cloneToken(this.tokens.continue), + loopType: util.cloneToken(this.tokens.loopType) + }) + ); + } } diff --git a/src/types/ArrayType.ts b/src/types/ArrayType.ts index 6c8210358..b2a5d7b22 100644 --- a/src/types/ArrayType.ts +++ b/src/types/ArrayType.ts @@ -44,4 +44,8 @@ export class ArrayType implements BscType { public toTypeString(): string { return 'object'; } + + public clone() { + return new ArrayType(...this.innerTypes?.map(x => x?.clone()) ?? []); + } } diff --git a/src/types/BooleanType.ts b/src/types/BooleanType.ts index f2027ea3f..caea6b49e 100644 --- a/src/types/BooleanType.ts +++ b/src/types/BooleanType.ts @@ -24,4 +24,8 @@ export class BooleanType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new BooleanType(this.typeText); + } } diff --git a/src/types/BscType.ts b/src/types/BscType.ts index 6760224b2..9fb796788 100644 --- a/src/types/BscType.ts +++ b/src/types/BscType.ts @@ -3,4 +3,5 @@ export interface BscType { isConvertibleTo(targetType: BscType): boolean; toString(): string; toTypeString(): string; + clone(): BscType; } diff --git a/src/types/CustomType.ts b/src/types/CustomType.ts index 9dad1d431..3285f5091 100644 --- a/src/types/CustomType.ts +++ b/src/types/CustomType.ts @@ -28,4 +28,8 @@ export class CustomType implements BscType { public isConvertibleTo(targetType: BscType) { return this.isAssignableTo(targetType); } + + public clone() { + return new CustomType(this.name); + } } diff --git a/src/types/DoubleType.ts b/src/types/DoubleType.ts index 9a49bfcc2..a63829312 100644 --- a/src/types/DoubleType.ts +++ b/src/types/DoubleType.ts @@ -36,4 +36,8 @@ export class DoubleType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new DoubleType(this.typeText); + } } diff --git a/src/types/DynamicType.ts b/src/types/DynamicType.ts index 845c73c4c..468e926c4 100644 --- a/src/types/DynamicType.ts +++ b/src/types/DynamicType.ts @@ -27,4 +27,8 @@ export class DynamicType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new DynamicType(this.typeText); + } } diff --git a/src/types/FloatType.ts b/src/types/FloatType.ts index 536b6e9e1..26edd47f5 100644 --- a/src/types/FloatType.ts +++ b/src/types/FloatType.ts @@ -37,4 +37,8 @@ export class FloatType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new FloatType(this.typeText); + } } diff --git a/src/types/FunctionType.ts b/src/types/FunctionType.ts index 4e8909bd2..c673d046c 100644 --- a/src/types/FunctionType.ts +++ b/src/types/FunctionType.ts @@ -76,4 +76,14 @@ export class FunctionType implements BscType { public toTypeString(): string { return 'Function'; } + + public clone() { + const result = new FunctionType(this.returnType); + for (let param of this.params) { + result.addParameter(param.name, param.type, param.isOptional); + } + result.isSub = this.isSub; + result.returnType = this.returnType?.clone(); + return result; + } } diff --git a/src/types/IntegerType.ts b/src/types/IntegerType.ts index 1a4aebca0..81d159986 100644 --- a/src/types/IntegerType.ts +++ b/src/types/IntegerType.ts @@ -37,4 +37,8 @@ export class IntegerType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new IntegerType(this.typeText); + } } diff --git a/src/types/InterfaceType.ts b/src/types/InterfaceType.ts index 77a92b3b3..0422a9eb7 100644 --- a/src/types/InterfaceType.ts +++ b/src/types/InterfaceType.ts @@ -68,4 +68,14 @@ export class InterfaceType implements BscType { } return false; } + + public clone() { + let members = new Map(); + for (const [key, member] of this.members) { + members.set(key, member?.clone()); + } + const result = new InterfaceType(members); + result.name = this.name; + return result; + } } diff --git a/src/types/InvalidType.ts b/src/types/InvalidType.ts index c7f76663c..842cff597 100644 --- a/src/types/InvalidType.ts +++ b/src/types/InvalidType.ts @@ -24,4 +24,8 @@ export class InvalidType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new InvalidType(this.typeText); + } } diff --git a/src/types/LongIntegerType.ts b/src/types/LongIntegerType.ts index feb1cafe4..f104102cf 100644 --- a/src/types/LongIntegerType.ts +++ b/src/types/LongIntegerType.ts @@ -37,4 +37,8 @@ export class LongIntegerType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new LongIntegerType(this.typeText); + } } diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index 283266347..4f0d7e242 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -24,4 +24,8 @@ export class ObjectType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new ObjectType(this.typeText); + } } diff --git a/src/types/StringType.ts b/src/types/StringType.ts index 4dea9e0cd..230261090 100644 --- a/src/types/StringType.ts +++ b/src/types/StringType.ts @@ -24,4 +24,8 @@ export class StringType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new StringType(this.typeText); + } } diff --git a/src/types/UninitializedType.ts b/src/types/UninitializedType.ts index e06442ea8..8b49654d1 100644 --- a/src/types/UninitializedType.ts +++ b/src/types/UninitializedType.ts @@ -20,4 +20,8 @@ export class UninitializedType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new UninitializedType(); + } } diff --git a/src/types/VoidType.ts b/src/types/VoidType.ts index f06621daf..6104913e0 100644 --- a/src/types/VoidType.ts +++ b/src/types/VoidType.ts @@ -24,4 +24,8 @@ export class VoidType implements BscType { public toTypeString(): string { return this.toString(); } + + public clone() { + return new VoidType(this.typeText); + } } diff --git a/src/util.ts b/src/util.ts index 07d513e20..49fed0606 100644 --- a/src/util.ts +++ b/src/util.ts @@ -998,6 +998,39 @@ export class Util { }; } + /** + * Clone a range + */ + public cloneRange(range: Range) { + if (range) { + return this.createRange(range.start.line, range.start.character, range.end.line, range.end.character); + } else { + return range; + } + } + + /** + * Clone every token + */ + public cloneToken(token: T) { + if (token) { + const result = { + kind: token.kind, + range: this.cloneRange(token.range), + text: token.text, + isReserved: token.isReserved, + leadingWhitespace: token.leadingWhitespace + } as T; + //handle those tokens that have charCode + if ('charCode' in token) { + (result as any).charCode = (token as any).charCode; + } + return result; + } else { + return token; + } + } + /** * Given a list of ranges, create a range that starts with the first non-null lefthand range, and ends with the first non-null * righthand range. Returns undefined if none of the items have a range. From f077e8b50389045f1f520978227a8a110d0d3638 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 25 Sep 2024 16:46:29 -0400 Subject: [PATCH 2/3] Update changelog for v0.67.7 --- CHANGELOG.md | 6 ++++++ package-lock.json | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc1643a5..a01d98ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.67.7](https://github.com/rokucommunity/brighterscript/compare/v0.67.6...v0.67.7) - 2024-09-25 +### Changed + - Ast node clone ([#1281](https://github.com/rokucommunity/brighterscript/pull/1281)) + + + ## [0.67.6](https://github.com/rokucommunity/brighterscript/compare/v0.67.5...v0.67.6) - 2024-09-05 ### Added - support for `roIntrinsicDouble` ([#1291](https://github.com/rokucommunity/brighterscript/pull/1291)) diff --git a/package-lock.json b/package-lock.json index 154d415f7..24971ab56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7045,9 +7045,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, "dependencies": { "isarray": "0.0.1" @@ -14380,9 +14380,9 @@ "dev": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, "requires": { "isarray": "0.0.1" From 3a2dc7282f76c23a4f9c0e00621374c7d4c89a40 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 25 Sep 2024 16:47:09 -0400 Subject: [PATCH 3/3] 0.67.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24971ab56..3ae0ae0be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "brighterscript", - "version": "0.67.6", + "version": "0.67.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "brighterscript", - "version": "0.67.6", + "version": "0.67.7", "license": "MIT", "dependencies": { "@rokucommunity/bslib": "^0.1.1", diff --git a/package.json b/package.json index 92e726ddf..39002ee65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brighterscript", - "version": "0.67.6", + "version": "0.67.7", "description": "A superset of Roku's BrightScript language.", "scripts": { "preversion": "npm run build && npm run lint && npm run test",