From 68fe1ab9382830fbfe3d58978f32098a0200a2d2 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 26 Jan 2024 14:08:08 -0400 Subject: [PATCH 1/5] Added tests --- src/files/BrsFile.spec.ts | 121 ++++++++++++++++++++++++++++++++++++++ src/parser/Expression.ts | 54 +++++++++++++++++ src/parser/Parser.ts | 76 ++++++++++++++++++++++++ 3 files changed, 251 insertions(+) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index bb1ca8557..0bad04d23 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3636,4 +3636,125 @@ describe('BrsFile', () => { range: util.createRange(4, 12, 4, 24) }]); }); + + + describe.only('backporting v1 syntax changes', () => { + + it('transpiles typed arrays to dynamic', () => { + testTranspile(` + sub main(param1 as string[], param2 as SomeType[]) + end sub + `, ` + sub main(param1 as dynamic, param2 as dynamic) + end sub + `); + }); + + it('transpiles multi-dimension typed arrays to dynamic', () => { + testTranspile(` + sub main(param1 as float[][][]) + end sub + `, ` + sub main(param1 as dynamic) + end sub + `); + }); + + it('removes typecasts in transpiled code', () => { + testTranspile(` + sub main(myNode, myString) + print (myNode as roSGNode).id + print (myNode as roSGNode).getParent().id + myNode2 = myNode as roSgNode + + print (myString as string).len() + print (myString as string).right(3) + myString2 = myString as string + end sub + `, ` + sub main(myNode, myString) + print myNode.id + print myNode.getParent().id + myNode2 = myNode + + print myString.len() + print myString.right(3) + myString2 = myString + end sub + `); + }); + + it('allows built in objects as type names', () => { + testTranspile(` + sub main(x as roSGNode, y as roSGNodeEvent, z as ifArray) + end sub + `, ` + sub main(x as dynamic, y as dynamic, z as dynamic) + end sub + `); + }); + + it('allows component names as types names', () => { + testTranspile(` + sub main(x as roSGNodeGroup, y as roSGNodeRowList, z as roSGNodeCustomComponent) + end sub + `, ` + sub main(x as dynamic, y as dynamic, z as dynamic) + end sub + `); + }); + + it('allows union types for primitives', () => { + testTranspile(` + sub main(x as string or float, y as object or float or string) + end sub + `, ` + sub main(x as dynamic, y as dynamic) + end sub + `); + }); + + it('allows union types for classes, interfaces', () => { + testTranspile(` + interface IFaceA + name as string + data as integer + end interface + + interface IFaceB + name as string + value as float + end interface + + sub main(x as IFaceA or IFaceB) + end sub + `, ` + sub main(x as dynamic) + end sub + `); + }); + + it('allows union types for classes, interfaces', () => { + testTranspile(` + namespace alpha.beta + interface IFaceA + name as string + data as integer + end interface + + interface IFaceB + name as string + value as float + end interface + end namespace + + sub main(x as alpha.beta.IFaceA or alpha.beta.IFaceB) + end sub + `, ` + sub main(x as dynamic) + end sub + `); + }); + + }); }); diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index acab57dc6..c95a38abd 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -1543,6 +1543,60 @@ export class RegexLiteralExpression extends Expression { } } +export class TypeExpression extends Expression implements TypedefProvider { + constructor( + /** + * The standard AST expression that represents the type for this TypeExpression. + */ + public typeToken: Token + ) { + super(); + this.range = typeToken?.range; + } + + + public range: Range; + + public transpile(state: BrsTranspileState): TranspileResult { + return [util.tokenToBscType(this.typeToken)?.toTypeString() ?? 'dynamic']; + } + public walk(visitor: WalkVisitor, options: WalkOptions) { + } + + getTypedef(state: TranspileState): (string | SourceNode)[] { + // TypeDefs should pass through any valid type names + return this.expression.transpile(state as BrsTranspileState); + } + +} + +export class TypeCastExpression extends Expression { + constructor( + public obj: Expression, + public asToken?: Token, + public typeExpression?: TypeExpression + ) { + super(); + this.range = util.createBoundingRange( + this.obj, + this.asToken, + this.typeExpression + ); + } + + public range: Range; + + public transpile(state: BrsTranspileState): TranspileResult { + return this.obj.transpile(state); + } + public walk(visitor: WalkVisitor, options: WalkOptions) { + if (options.walkMode & InternalWalkMode.walkExpressions) { + walk(this, 'obj', visitor, options); + walk(this, 'typeExpression', visitor, options); + } + } +} + // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style type ExpressionValue = string | number | boolean | Expression | ExpressionValue[] | { [key: string]: ExpressionValue }; diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index fb043e0f9..ad046d842 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -2575,6 +2575,82 @@ export class Parser { return expression; } + /** + * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType + */ + private typeExpression(): TypeExpression { + const changedTokens: { token: Token; oldKind: TokenKind }[] = []; + try { + let expr: Expression = this.getTypeExpressionPart(changedTokens); + while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) { + // If we're in Brighterscript mode, allow union types with "or" between types + // TODO: Handle Union types in parens? eg. "(string or integer)" + let operator = this.previous(); + let right = this.getTypeExpressionPart(changedTokens); + if (right) { + expr = new BinaryExpression(expr, operator, right); + } else { + break; + } + } + if (expr) { + return new TypeExpression(expr); + } + + } catch (error) { + // Something went wrong - reset the kind to what it was previously + for (const changedToken of changedTokens) { + changedToken.token.kind = changedToken.oldKind; + } + throw error; + } + } + + /** + * Gets a single "part" of a type of a potential Union type + * Note: this does not NEED to be part of a union type, but the logic is the same + * + * @param changedTokens an array that is modified with any tokens that have been changed from their default kind to identifiers - eg. when a keyword is used as type + * @returns an expression that was successfully parsed + */ + private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) { + let expr: VariableExpression | DottedGetExpression | TypedArrayExpression; + if (this.checkAny(...DeclarableTypes)) { + // if this is just a type, just use directly + expr = new VariableExpression(this.advance() as Identifier); + } else { + if (this.checkAny(...AllowedTypeIdentifiers)) { + // Since the next token is allowed as a type identifier, change the kind + let nextToken = this.peek(); + changedTokens.push({ token: nextToken, oldKind: nextToken.kind }); + nextToken.kind = TokenKind.Identifier; + } + expr = this.identifyingExpression(AllowedTypeIdentifiers); + } + + //Check if it has square brackets, thus making it an array + if (expr && this.check(TokenKind.LeftSquareBracket)) { + if (this.options.mode === ParseMode.BrightScript) { + // typed arrays not allowed in Brightscript + this.warnIfNotBrighterScriptMode('typed arrays'); + return expr; + } + + // Check if it is an array - that is, if it has `[]` after the type + // eg. `string[]` or `SomeKlass[]` + // This is while loop, so it supports multidimensional arrays (eg. integer[][]) + while (this.check(TokenKind.LeftSquareBracket)) { + const leftBracket = this.advance(); + if (this.check(TokenKind.RightSquareBracket)) { + const rightBracket = this.advance(); + expr = new TypedArrayExpression(expr, leftBracket, rightBracket); + } + } + } + + return expr; + } + /** * Tries to get the next token as a type * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode From 0da82a9138bb2f162f6ffdde4db5f1f902d36b66 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sun, 28 Jan 2024 21:53:27 -0400 Subject: [PATCH 2/5] Unions and typed arrays are accepted --- src/files/BrsFile.spec.ts | 2 +- src/parser/Parser.ts | 131 ++++++++++++-------------------------- 2 files changed, 43 insertions(+), 90 deletions(-) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 0bad04d23..65c82025a 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3704,7 +3704,7 @@ describe('BrsFile', () => { `); }); - it('allows union types for primitives', () => { + it.only('allows union types for primitives', () => { testTranspile(` sub main(x as string or float, y as object or float or string) end sub diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index ad046d842..02d2e3632 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -2575,81 +2575,7 @@ export class Parser { return expression; } - /** - * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType - */ - private typeExpression(): TypeExpression { - const changedTokens: { token: Token; oldKind: TokenKind }[] = []; - try { - let expr: Expression = this.getTypeExpressionPart(changedTokens); - while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) { - // If we're in Brighterscript mode, allow union types with "or" between types - // TODO: Handle Union types in parens? eg. "(string or integer)" - let operator = this.previous(); - let right = this.getTypeExpressionPart(changedTokens); - if (right) { - expr = new BinaryExpression(expr, operator, right); - } else { - break; - } - } - if (expr) { - return new TypeExpression(expr); - } - - } catch (error) { - // Something went wrong - reset the kind to what it was previously - for (const changedToken of changedTokens) { - changedToken.token.kind = changedToken.oldKind; - } - throw error; - } - } - - /** - * Gets a single "part" of a type of a potential Union type - * Note: this does not NEED to be part of a union type, but the logic is the same - * - * @param changedTokens an array that is modified with any tokens that have been changed from their default kind to identifiers - eg. when a keyword is used as type - * @returns an expression that was successfully parsed - */ - private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) { - let expr: VariableExpression | DottedGetExpression | TypedArrayExpression; - if (this.checkAny(...DeclarableTypes)) { - // if this is just a type, just use directly - expr = new VariableExpression(this.advance() as Identifier); - } else { - if (this.checkAny(...AllowedTypeIdentifiers)) { - // Since the next token is allowed as a type identifier, change the kind - let nextToken = this.peek(); - changedTokens.push({ token: nextToken, oldKind: nextToken.kind }); - nextToken.kind = TokenKind.Identifier; - } - expr = this.identifyingExpression(AllowedTypeIdentifiers); - } - - //Check if it has square brackets, thus making it an array - if (expr && this.check(TokenKind.LeftSquareBracket)) { - if (this.options.mode === ParseMode.BrightScript) { - // typed arrays not allowed in Brightscript - this.warnIfNotBrighterScriptMode('typed arrays'); - return expr; - } - // Check if it is an array - that is, if it has `[]` after the type - // eg. `string[]` or `SomeKlass[]` - // This is while loop, so it supports multidimensional arrays (eg. integer[][]) - while (this.check(TokenKind.LeftSquareBracket)) { - const leftBracket = this.advance(); - if (this.check(TokenKind.RightSquareBracket)) { - const rightBracket = this.advance(); - expr = new TypedArrayExpression(expr, leftBracket, rightBracket); - } - } - } - - return expr; - } /** * Tries to get the next token as a type @@ -2658,24 +2584,51 @@ export class Parser { */ private typeToken(): Token { let typeToken: Token; - - if (this.checkAny(...DeclarableTypes)) { - // Token is a built in type - typeToken = this.advance(); - } else if (this.options.mode === ParseMode.BrighterScript) { - try { - // see if we can get a namespaced identifer - const qualifiedType = this.getNamespacedVariableNameExpression(); - typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range); - } catch { - //could not get an identifier - just get whatever's next + let lookForUnions = true; + let isAUnion = false; + let resultToken; + while (lookForUnions) { + lookForUnions = false; + + if (this.checkAny(...DeclarableTypes)) { + // Token is a built in type + typeToken = this.advance(); + } else if (this.options.mode === ParseMode.BrighterScript) { + try { + // see if we can get a namespaced identifer + const qualifiedType = this.getNamespacedVariableNameExpression(); + typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range); + } catch { + //could not get an identifier - just get whatever's next + typeToken = this.advance(); + } + } else { + // just get whatever's next typeToken = this.advance(); } - } else { - // just get whatever's next - typeToken = this.advance(); + resultToken = resultToken ?? typeToken; + if (resultToken && this.options.mode === ParseMode.BrighterScript) { + // check for brackets + while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) { + const leftBracket = this.advance(); + const rightBracket = this.advance(); + typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket)); + resultToken = createToken(TokenKind.Dynamic, null, typeToken.range); + } + + if (this.check(TokenKind.Or)) { + lookForUnions = true; + let orToken = this.advance(); + resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken)); + isAUnion = true; + } + + } + } + if (isAUnion) { + resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken)); } - return typeToken; + return resultToken; } private primary(): Expression { From 756fe033c9b8da0be06263798ac909a01791fe1a Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 29 Jan 2024 10:34:17 -0400 Subject: [PATCH 3/5] Allows v1 type syntax, with no validation --- src/Scope.ts | 5 ++++- src/astUtils/reflection.ts | 5 ++++- src/files/BrsFile.spec.ts | 46 ++++++++++++++++++++++++++++++++------ src/parser/Expression.ts | 38 ++++++------------------------- src/parser/Parser.ts | 32 +++++++++++++++++++++----- src/util.ts | 10 +++++++++ 6 files changed, 90 insertions(+), 46 deletions(-) diff --git a/src/Scope.ts b/src/Scope.ts index f6a045f17..5d8373cd7 100644 --- a/src/Scope.ts +++ b/src/Scope.ts @@ -891,7 +891,10 @@ export class Scope { if (isCustomType(param.type) && param.typeToken) { const paramTypeName = param.type.name; const currentNamespaceName = func.findAncestor(isNamespaceStatement)?.getName(ParseMode.BrighterScript); - if (!this.hasClass(paramTypeName, currentNamespaceName) && !this.hasInterface(paramTypeName) && !this.hasEnum(paramTypeName)) { + // check for built in types + const isBuiltInType = util.isBuiltInType(paramTypeName); + + if (!isBuiltInType && !this.hasClass(paramTypeName, currentNamespaceName) && !this.hasInterface(paramTypeName) && !this.hasEnum(paramTypeName)) { this.diagnostics.push({ ...DiagnosticMessages.functionParameterTypeIsInvalid(param.name.text, paramTypeName), range: param.typeToken.range, diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 91d0bdfc5..d262dbbfd 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 } from '../parser/Expression'; +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 { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; import type { BscFile, File, TypedefProvider } from '../interfaces'; @@ -260,6 +260,9 @@ export function isAnnotationExpression(element: AstNode | undefined): element is export function isTypedefProvider(element: any): element is TypedefProvider { return 'getTypedef' in element; } +export function isTypeCastExpression(element: any): element is TypeCastExpression { + return element?.constructor.name === 'TypeCastExpression'; +} // BscType reflection export function isStringType(value: any): value is StringType { diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 65c82025a..be24a48f4 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3638,7 +3638,7 @@ describe('BrsFile', () => { }); - describe.only('backporting v1 syntax changes', () => { + describe('backporting v1 syntax changes', () => { it('transpiles typed arrays to dynamic', () => { testTranspile(` @@ -3666,7 +3666,6 @@ describe('BrsFile', () => { print (myNode as roSGNode).id print (myNode as roSGNode).getParent().id myNode2 = myNode as roSgNode - print (myString as string).len() print (myString as string).right(3) myString2 = myString as string @@ -3676,20 +3675,31 @@ describe('BrsFile', () => { print myNode.id print myNode.getParent().id myNode2 = myNode - - print myString.len() + print myString.len() print myString.right(3) myString2 = myString end sub `); }); + it('allows and removes multiple typecasts in transpiled code', () => { + testTranspile(` + sub main(myNode) + print ((myNode as roSGNode as roSGNodeLabel).text as string as ifStringOps).len() + end sub + `, ` + sub main(myNode) + print myNode.text.len() + end sub + `); + }); + it('allows built in objects as type names', () => { testTranspile(` sub main(x as roSGNode, y as roSGNodeEvent, z as ifArray) end sub `, ` - sub main(x as dynamic, y as dynamic, z as dynamic) + sub main(x as object, y as object, z as object) end sub `); }); @@ -3699,12 +3709,12 @@ describe('BrsFile', () => { sub main(x as roSGNodeGroup, y as roSGNodeRowList, z as roSGNodeCustomComponent) end sub `, ` - sub main(x as dynamic, y as dynamic, z as dynamic) + sub main(x as object, y as object, z as object) end sub `); }); - it.only('allows union types for primitives', () => { + it('allows union types for primitives', () => { testTranspile(` sub main(x as string or float, y as object or float or string) end sub @@ -3756,5 +3766,27 @@ describe('BrsFile', () => { `); }); + it('allows union types of arrays', () => { + testTranspile(` + namespace alpha.beta + interface IFaceA + name as string + data as integer + end interface + + interface IFaceB + name as string + value as float + end interface + end namespace + + sub main(x as alpha.beta.IFaceA[][] or alpha.beta.IFaceB[] or ifStringOps) + end sub + `, ` + sub main(x as dynamic) + end sub + `); + }); + }); }); diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index c95a38abd..7c7ed6602 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, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; +import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isStringType, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; import type { TranspileResult, TypedefProvider } from '../interfaces'; import { VoidType } from '../types/VoidType'; import { DynamicType } from '../types/DynamicType'; @@ -554,6 +554,9 @@ export class GroupingExpression extends Expression { public readonly range: Range; transpile(state: BrsTranspileState) { + if (isTypeCastExpression(this.expression)) { + return this.expression.transpile(state); + } return [ state.transpileToken(this.tokens.left), ...this.expression.transpile(state), @@ -1543,44 +1546,18 @@ export class RegexLiteralExpression extends Expression { } } -export class TypeExpression extends Expression implements TypedefProvider { - constructor( - /** - * The standard AST expression that represents the type for this TypeExpression. - */ - public typeToken: Token - ) { - super(); - this.range = typeToken?.range; - } - - - public range: Range; - - public transpile(state: BrsTranspileState): TranspileResult { - return [util.tokenToBscType(this.typeToken)?.toTypeString() ?? 'dynamic']; - } - public walk(visitor: WalkVisitor, options: WalkOptions) { - } - - getTypedef(state: TranspileState): (string | SourceNode)[] { - // TypeDefs should pass through any valid type names - return this.expression.transpile(state as BrsTranspileState); - } - -} export class TypeCastExpression extends Expression { constructor( public obj: Expression, - public asToken?: Token, - public typeExpression?: TypeExpression + public asToken: Token, + public typeToken: Token ) { super(); this.range = util.createBoundingRange( this.obj, this.asToken, - this.typeExpression + this.typeToken ); } @@ -1592,7 +1569,6 @@ export class TypeCastExpression extends Expression { public walk(visitor: WalkVisitor, options: WalkOptions) { if (options.walkMode & InternalWalkMode.walkExpressions) { walk(this, 'obj', visitor, options); - walk(this, 'typeExpression', visitor, options); } } } diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 02d2e3632..cdd73da53 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -83,6 +83,7 @@ import { TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, + TypeCastExpression, UnaryExpression, VariableExpression, XmlAttributeGetExpression @@ -1004,7 +1005,7 @@ export class Parser { // parse argument default value if (this.match(TokenKind.Equal)) { // it seems any expression is allowed here -- including ones that operate on other arguments! - defaultValue = this.expression(); + defaultValue = this.expression(false); } let asToken = null; @@ -2277,8 +2278,29 @@ export class Parser { this.pendingAnnotations = parentAnnotations; } - private expression(): Expression { - const expression = this.anonymousFunction(); + private expression(findTypeCast = true): Expression { + let expression = this.anonymousFunction(); + let asToken: Token; + let typeToken: Token; + if (findTypeCast) { + do { + if (this.check(TokenKind.As)) { + this.warnIfNotBrighterScriptMode('type cast'); + // Check if this expression is wrapped in any type casts + // allows for multiple casts: + // myVal = foo() as dynamic as string + + asToken = this.advance(); + typeToken = this.typeToken(); + if (asToken && typeToken) { + expression = new TypeCastExpression(expression, asToken, typeToken); + } + } else { + break; + } + + } while (asToken && typeToken); + } this._references.expressions.add(expression); return expression; } @@ -2575,12 +2597,11 @@ export class Parser { return expression; } - - /** * Tries to get the next token as a type * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode * Will return a token of whatever is next to be parsed + * Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way */ private typeToken(): Token { let typeToken: Token; @@ -2622,7 +2643,6 @@ export class Parser { resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken)); isAUnion = true; } - } } if (isAUnion) { diff --git a/src/util.ts b/src/util.ts index e158b7d9f..c3b986b6f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -35,6 +35,7 @@ import * as requireRelative from 'require-relative'; import type { BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; import type { AstNode, Expression, Statement } from './parser/AstNode'; +import { components, events, interfaces } from './roku-types'; export class Util { public clearConsole() { @@ -1527,6 +1528,15 @@ export class Util { }]); } } + + public isBuiltInType(typeName: string) { + const typeNameLower = typeName.toLowerCase(); + if (typeNameLower.startsWith('rosgnode')) { + // NOTE: this is unsafe and only used to avoid validation errors in backported v1 type syntax + return true; + } + return components[typeNameLower] || interfaces[typeNameLower] || events[typeNameLower]; + } } /** From 33d1b75b1621d4230b7dee49ae729dc31f886fe4 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 29 Jan 2024 14:54:18 -0400 Subject: [PATCH 4/5] Fixed 2 more errors --- src/Scope.ts | 4 +- src/bscPlugin/validation/BrsFileValidator.ts | 3 +- src/files/BrsFile.spec.ts | 53 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Scope.ts b/src/Scope.ts index 5d8373cd7..f575551dd 100644 --- a/src/Scope.ts +++ b/src/Scope.ts @@ -878,7 +878,9 @@ export class Scope { // check if this custom type is in our class map const returnTypeName = func.returnType.name; const currentNamespaceName = func.findAncestor(isNamespaceStatement)?.getName(ParseMode.BrighterScript); - if (!this.hasClass(returnTypeName, currentNamespaceName) && !this.hasInterface(returnTypeName) && !this.hasEnum(returnTypeName)) { + // check for built in types + const isBuiltInType = util.isBuiltInType(returnTypeName); + if (!isBuiltInType && !this.hasClass(returnTypeName, currentNamespaceName) && !this.hasInterface(returnTypeName) && !this.hasEnum(returnTypeName)) { this.diagnostics.push({ ...DiagnosticMessages.invalidFunctionReturnType(returnTypeName), range: func.returnTypeToken.range, diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 15d56c536..d76d32130 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -9,6 +9,7 @@ import type { LiteralExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement'; import { DynamicType } from '../../types/DynamicType'; +import { InterfaceType } from '../../types/InterfaceType'; import util from '../../util'; import type { Range } from 'vscode-languageserver'; @@ -120,10 +121,10 @@ export class BrsFileValidator { }, InterfaceStatement: (node) => { this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name)); + node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map())); }, ConstStatement: (node) => { this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name)); - node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance); }, CatchStatement: (node) => { diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index be24a48f4..39482764d 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3650,6 +3650,18 @@ describe('BrsFile', () => { `); }); + it('transpiles typed arrays in return types to dynamic', () => { + testTranspile(` + function main() as integer[] + return [] + end function + `, ` + function main() as dynamic + return [] + end function + `); + }); + it('transpiles multi-dimension typed arrays to dynamic', () => { testTranspile(` sub main(param1 as float[][][]) @@ -3788,5 +3800,46 @@ describe('BrsFile', () => { `); }); + it('allows built-in types for return values', () => { + testTranspile(` + function makeLabel(text as string) as roSGNodeLabel + label = createObject("roSGNode", "Label") + label.text = text + end function + `, ` + function makeLabel(text as string) as object + label = createObject("roSGNode", "Label") + label.text = text + end function + `); + }); + + it('allows extends on interfaces', () => { + testTranspile(` + interface MyBase + url as string + end interface + + interface MyExtends extends MyBase + method as string + end interface + `, ` + `); + }); + + it('allows extends on classes', () => { + const file = program.setFile('source/main.bs', ` + class MyBase + url as string + end class + + class MyExtends extends MyBase + method as string + end class + `); + program.validate(); + expectZeroDiagnostics(program); + }); + }); }); From d3465c2d686758fd41a3fcf2445e4fd031f365b6 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 29 Jan 2024 15:17:18 -0400 Subject: [PATCH 5/5] Fixed lint error --- src/files/BrsFile.spec.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 39482764d..a590f90e3 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3662,6 +3662,18 @@ describe('BrsFile', () => { `); }); + it('transpiles typed arrays in return types to dynamic', () => { + testTranspile(` + function main() as integer[] + return [] + end function + `, ` + function main() as dynamic + return [] + end function + `); + }); + it('transpiles multi-dimension typed arrays to dynamic', () => { testTranspile(` sub main(param1 as float[][][]) @@ -3828,7 +3840,7 @@ describe('BrsFile', () => { }); it('allows extends on classes', () => { - const file = program.setFile('source/main.bs', ` + program.setFile('source/main.bs', ` class MyBase url as string end class