diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts index 6e5c4a7cf..fe1b9cd20 100644 --- a/src/files/BrsFile.Class.spec.ts +++ b/src/files/BrsFile.Class.spec.ts @@ -586,6 +586,132 @@ describe('BrsFile BrighterScript classes', () => { `, undefined, 'source/main.bs'); }); + it('does not crash synthesizing an implicit constructor whose inherited param is a class type declared in a different namespace', async () => { + await testTranspile(` + namespace Foo + class Thing + end class + + class Base + sub new(t as Thing) + m.thing = t + end sub + thing as Thing + end class + end namespace + + namespace Bar + class DerivedImplicitCtor extends Foo.Base + extra as integer = 2 + end class + end namespace + `, ` + sub __Foo_Thing_method_new() + end sub + function __Foo_Thing_builder() + instance = {} + instance.new = __Foo_Thing_method_new + return instance + end function + function Foo_Thing() + instance = __Foo_Thing_builder() + instance.new() + return instance + end function + sub __Foo_Base_method_new(t as dynamic) + m.thing = invalid + m.thing = t + end sub + function __Foo_Base_builder() + instance = {} + instance.new = __Foo_Base_method_new + return instance + end function + function Foo_Base(t as dynamic) + instance = __Foo_Base_builder() + instance.new(t) + return instance + end function + sub __Bar_DerivedImplicitCtor_method_new(t as dynamic) + m.super0_new(t) + m.extra = 2 + end sub + function __Bar_DerivedImplicitCtor_builder() + instance = __Foo_Base_builder() + instance.super0_new = instance.new + instance.new = __Bar_DerivedImplicitCtor_method_new + return instance + end function + function Bar_DerivedImplicitCtor(t as dynamic) + instance = __Bar_DerivedImplicitCtor_builder() + instance.new(t) + return instance + end function + `, undefined, 'source/main.bs'); + }); + + it('does not crash synthesizing an implicit constructor whose inherited param is a class type declared in the same namespace', async () => { + await testTranspile(` + namespace Foo + class Thing + end class + + class Base + sub new(t as Thing) + m.thing = t + end sub + thing as Thing + end class + + class DerivedSameNs extends Base + extra as integer = 2 + end class + end namespace + `, ` + sub __Foo_Thing_method_new() + end sub + function __Foo_Thing_builder() + instance = {} + instance.new = __Foo_Thing_method_new + return instance + end function + function Foo_Thing() + instance = __Foo_Thing_builder() + instance.new() + return instance + end function + sub __Foo_Base_method_new(t as dynamic) + m.thing = invalid + m.thing = t + end sub + function __Foo_Base_builder() + instance = {} + instance.new = __Foo_Base_method_new + return instance + end function + function Foo_Base(t as dynamic) + instance = __Foo_Base_builder() + instance.new(t) + return instance + end function + sub __Foo_DerivedSameNs_method_new(t as dynamic) + m.super0_new(t) + m.extra = 2 + end sub + function __Foo_DerivedSameNs_builder() + instance = __Foo_Base_builder() + instance.super0_new = instance.new + instance.new = __Foo_DerivedSameNs_method_new + return instance + end function + function Foo_DerivedSameNs(t as dynamic) + instance = __Foo_DerivedSameNs_builder() + instance.new(t) + return instance + end function + `, undefined, 'source/main.bs'); + }); + it('properly handles child class constructor override and super calls', async () => { await testTranspile(` class Animal diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 43ac53c3f..de41f6e94 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -2693,9 +2693,16 @@ export class TypeExpression extends Expression implements TypedefProvider { * The standard AST expression that represents the type for this TypeExpression. */ expression: Expression; + /** + * An already-known type for this TypeExpression, bypassing resolution of `expression` + * via symbol table lookup. Useful when `expression` is not attached to (or can't resolve + * against) a real symbol table - e.g. a type reference synthesized for a detached AST node. + */ + resolvedType?: BscType; }) { super(); this.expression = options.expression; + this.resolvedType = options.resolvedType; this.location = util.cloneLocation(this.expression?.location); } @@ -2706,6 +2713,12 @@ export class TypeExpression extends Expression implements TypedefProvider { */ public readonly expression: Expression; + /** + * An already-known type for this TypeExpression. When set, `getType()` returns this + * directly instead of resolving `expression` via symbol table lookup. + */ + public readonly resolvedType?: BscType; + public readonly location: Location; public transpile(state: BrsTranspileState): TranspileResult { @@ -2729,7 +2742,7 @@ export class TypeExpression extends Expression implements TypedefProvider { } public getType(options: GetTypeOptions): BscType { - return this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime }); + return this.resolvedType ?? this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime }); } getTypedef(state: TranspileState): TranspileResult { @@ -2760,7 +2773,8 @@ export class TypeExpression extends Expression implements TypedefProvider { public clone() { return this.finalizeClone( new TypeExpression({ - expression: this.expression?.clone() + expression: this.expression?.clone(), + resolvedType: this.resolvedType }), ['expression'] ); diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index ac79ac1ac..113923ed6 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -1,8 +1,8 @@ /* eslint-disable no-bitwise */ import type { Token, Identifier } from '../lexer/Token'; import { TokenKind } from '../lexer/TokenKind'; -import type { DottedGetExpression, FunctionParameterExpression, LiteralExpression, TypecastExpression, TypeExpression } from './Expression'; -import { FunctionExpression } from './Expression'; +import type { DottedGetExpression, LiteralExpression, TypecastExpression } from './Expression'; +import { FunctionExpression, FunctionParameterExpression, TypeExpression } from './Expression'; import { CallExpression, VariableExpression } from './Expression'; import { util } from '../util'; import type { Location } from 'vscode-languageserver'; @@ -10,10 +10,10 @@ import type { BrsTranspileState } from './BrsTranspileState'; import { ParseMode } from './Parser'; import type { WalkVisitor, WalkOptions } from '../astUtils/visitors'; import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors'; -import { isCallExpression, isCatchStatement, isConditionalCompileStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isPrintSeparatorExpression, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isUninitializedType, isVoidType, isWhileStatement } from '../astUtils/reflection'; +import { isCallExpression, isCatchStatement, isClassType, isConditionalCompileStatement, isEnumMemberStatement, isEnumType, isEnumStatement, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isPrintSeparatorExpression, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isUninitializedType, isVoidType, isWhileStatement } from '../astUtils/reflection'; import type { GetTypeOptions } from '../interfaces'; import { TypeChainEntry, type TranspileResult, type TypedefProvider } from '../interfaces'; -import { createIdentifier, createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators'; +import { createDottedIdentifier, createIdentifier, createInvalidLiteral, createMethodStatement, createToken, createVariableExpression } from '../astUtils/creators'; import { DynamicType } from '../types/DynamicType'; import type { BscType } from '../types/BscType'; import { SymbolTable } from '../SymbolTable'; @@ -2867,6 +2867,31 @@ export class ClassStatement extends Statement implements TypedefProvider { return []; } + /** + * Clone a parent constructor's parameter for use in a synthesized subclass constructor. + * The clone is detached from the AST (no parent), so if the parameter's type is a class + * or enum, its type reference can't be re-resolved by symbol name once cloned - especially + * since the subclass may live in a different namespace than the one the parameter's type + * was originally declared in, where even a fully-qualified reference has no symbol table + * to resolve against. Resolve the type now (while the original is still properly attached), + * and bake the fully-qualified name/type directly into the clone. + */ + private cloneConstructorParam(param: FunctionParameterExpression): FunctionParameterExpression { + const exprType = param.typeExpression?.getType({ flags: SymbolTypeFlag.typetime }); + if (!isClassType(exprType) && !isEnumType(exprType)) { + return param.clone(); + } + const nameParts = exprType.toString().split('.'); + const qualifiedExpression = nameParts.length > 1 ? createDottedIdentifier(nameParts) : createVariableExpression(nameParts[0]); + return new FunctionParameterExpression({ + name: param.tokens.name, + equals: param.tokens.equals, + defaultValue: param.defaultValue?.clone(), + as: param.tokens.as, + typeExpression: new TypeExpression({ expression: qualifiedExpression, resolvedType: exprType }) + }); + } + /** * Determine if the specified field was declared in one of the ancestor classes */ @@ -2999,7 +3024,7 @@ export class ClassStatement extends Statement implements TypedefProvider { modifiers: [], name: createIdentifier('new'), func: new FunctionExpression({ - parameters: params.map(x => x.clone()), + parameters: params.map(x => this.cloneConstructorParam(x)), body: new Block({ statements: [call] }), functionType: createToken(TokenKind.Sub), endFunctionType: createToken(TokenKind.EndSub),