diff --git a/package-lock.json b/package-lock.json index c549cfe35..aaf660dfd 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", @@ -8098,9 +8098,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", @@ -15228,9 +15228,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 3960b5386..648ebf04e 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/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 0f997f0c9..c6f654e18 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -6,9 +6,11 @@ import { expect } from '../chai-config.spec'; import type { DottedGetExpression } 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 { isAssignmentStatement, isBlock, isBody, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isIncrementStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isMethodStatement, isPrintStatement, isThrowStatement } from '../astUtils/reflection'; +import type { ClassStatement, FunctionStatement, InterfaceFieldStatement, InterfaceMethodStatement, MethodStatement, InterfaceStatement, TryCatchStatement, CatchStatement, ThrowStatement, EnumStatement, EnumMemberStatement, ConstStatement, Body, Block, CommentStatement, ExpressionStatement, IfStatement, IncrementStatement, PrintStatement } from './Statement'; +import { AssignmentStatement, EmptyStatement } from './Statement'; +import { ParseMode, Parser } from './Parser'; +import type { AstNode } from './AstNode'; describe('AstNode', () => { let program: Program; @@ -184,4 +186,511 @@ describe('AstNode', () => { expect(count).to.eql(1); }); }); + + describe.only('clone', () => { + function testClone(code: string | AstNode) { + let original: AstNode; + if (typeof code === 'string') { + const parser = Parser.parse(code, { mode: ParseMode.BrighterScript }); + original = parser.ast; + expectZeroDiagnostics(parser); + } else { + original = code; + } + + const clone = original.clone(); + //ensure the clone is identical to the original + + //compare them both ways to ensure no extra properties exist + ensureIdentical(original, clone); + ensureIdentical(clone, original); + + 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, skip it (it's redundant anyway) + (isFunctionExpression(original) && key === 'functionStatement') + ) { + continue; + } + + 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); + } else if ([''].includes(typeOfValue)) { + //primitive values should be identical + expect(cloneValue).to.equal(originalValue, `${fullKey} should be equal`); + } + } + } + } + + 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; + 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 ExitFor statement', () => { + testClone(` + sub main() + for i = 0 to 10 + exit for + end for + 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 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 + `); + }); + + }); }); diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 80588c71f..c9d5769ca 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -239,7 +239,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; @@ -377,7 +377,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; @@ -680,7 +680,7 @@ export class PrintStatement extends Statement { }, this.expressions?.map(e => { if (isExpression(e as any)) { - return (e as Expression)?.clone(); + return (e as Expression).clone(); } else { return util.cloneToken(e as Token); } @@ -1515,7 +1515,7 @@ export class InterfaceStatement extends Statement implements TypedefProvider { this.tokens.name, this.tokens.extends, this.parentInterfaceName, - ...this.body, + ...this.body ?? [], this.tokens.endInterface ); } @@ -1740,7 +1740,7 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide util.cloneToken(this.tokens.name), util.cloneToken(this.tokens.as), util.cloneToken(this.tokens.type), - this.type.clone(), + this.type?.clone(), util.cloneToken(this.tokens.optional) ); }