diff --git a/src/Scope.ts b/src/Scope.ts index f6a045f17..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, @@ -891,7 +893,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/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 bb1ca8557..a590f90e3 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3636,4 +3636,222 @@ describe('BrsFile', () => { range: util.createRange(4, 12, 4, 24) }]); }); + + + describe('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 typed arrays in return types to dynamic', () => { + testTranspile(` + function main() as integer[] + return [] + end function + `, ` + function main() as dynamic + return [] + end function + `); + }); + + 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[][][]) + 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 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 object, y as object, z as object) + 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 object, y as object, z as object) + 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 + `); + }); + + 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 + `); + }); + + 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', () => { + 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); + }); + + }); }); diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index acab57dc6..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,6 +1546,33 @@ export class RegexLiteralExpression extends Expression { } } + +export class TypeCastExpression extends Expression { + constructor( + public obj: Expression, + public asToken: Token, + public typeToken: Token + ) { + super(); + this.range = util.createBoundingRange( + this.obj, + this.asToken, + this.typeToken + ); + } + + 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); + } + } +} + // 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..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; } @@ -2579,27 +2601,54 @@ export class Parser { * 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; - - 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 { 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]; + } } /**