diff --git a/src/Program.ts b/src/Program.ts index 587d6d852..a7352ec78 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -645,7 +645,9 @@ export class Program { this.logger.time(LogLevel.info, ['Validate all scopes'], () => { for (let scopeName in this.scopes) { let scope = this.scopes[scopeName]; + scope.linkSymbolTable(); scope.validate(); + scope.unlinkSymbolTable(); } }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index d26e43228..55340c8a6 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,10 +1,11 @@ import { isClassStatement, isCommentStatement, isConstStatement, isEnumStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespaceStatement } from '../../astUtils/reflection'; +import { WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; -import type { BsDiagnostic, OnFileValidateEvent } from '../../interfaces'; +import type { OnFileValidateEvent } from '../../interfaces'; import { TokenKind } from '../../lexer/TokenKind'; import type { LiteralExpression } from '../../parser/Expression'; -import type { EnumMemberStatement } from '../../parser/Statement'; +import type { EnumMemberStatement, EnumStatement } from '../../parser/Statement'; export class BrsFileValidator { constructor( @@ -13,43 +14,66 @@ export class BrsFileValidator { } public process() { - this.validateEnumDeclarations(); + this.walk(); this.flagTopLevelStatements(); } - private validateEnumDeclarations() { - const diagnostics = [] as BsDiagnostic[]; - for (const stmt of this.event.file.parser.references.enumStatements) { - const members = stmt.getMembers(); - //the enum data type is based on the first member value - const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral; - const memberNames = new Set(); - for (const member of members) { - const memberNameLower = member.name?.toLowerCase(); + /** + * Walk the full AST + */ + private walk() { + this.event.file.ast.walk((node, parent) => { + // link every child with its parent + node.parent = parent; - /** - * flag duplicate member names - */ - if (memberNames.has(memberNameLower)) { - diagnostics.push({ - ...DiagnosticMessages.duplicateIdentifier(member.name), - file: this.event.file, - range: member.range - }); - } else { - memberNames.add(memberNameLower); + //do some file-based validations + if (isEnumStatement(node)) { + this.validateEnumDeclaration(node); + } else if (isNamespaceStatement(node)) { + //namespace names shouldn't be walked, but it needs its parent assigned + node.nameExpression.parent = parent; + } else if (isClassStatement(node)) { + //class extends names don't get walked, but it needs its parent + if (node.parentClassName) { + node.parentClassName.parent = parent; } + } else if (isInterfaceStatement(node)) { + //class extends names don't get walked, but it needs its parent + if (node.parentInterfaceName) { + node.parentInterfaceName.parent = parent; + } + } + }, { + walkMode: WalkMode.visitAllRecursive + }); + } - //Enforce all member values are the same type - this.validateEnumValueTypes(diagnostics, member, enumValueKind); + private validateEnumDeclaration(stmt: EnumStatement) { + const members = stmt.getMembers(); + //the enum data type is based on the first member value + const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral; + const memberNames = new Set(); + for (const member of members) { + const memberNameLower = member.name?.toLowerCase(); + /** + * flag duplicate member names + */ + if (memberNames.has(memberNameLower)) { + this.event.file.addDiagnostic({ + ...DiagnosticMessages.duplicateIdentifier(member.name), + range: member.range + }); + } else { + memberNames.add(memberNameLower); } + //Enforce all member values are the same type + this.validateEnumValueTypes(member, enumValueKind); } - this.event.file.addDiagnostics(diagnostics); } - private validateEnumValueTypes(diagnostics: BsDiagnostic[], member: EnumMemberStatement, enumValueKind: TokenKind) { + private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) { const memberValueKind = (member.value as LiteralExpression)?.token?.kind; if ( @@ -58,8 +82,7 @@ export class BrsFileValidator { //has value, that value is not a literal (member.value && !isLiteralExpression(member.value)) ) { - diagnostics.push({ - file: this.event.file, + this.event.file.addDiagnostic({ ...DiagnosticMessages.enumValueMustBeType( enumValueKind.replace(/literal$/i, '').toLowerCase() ), @@ -73,8 +96,7 @@ export class BrsFileValidator { if (memberValueKind) { //member value is same as enum if (memberValueKind !== enumValueKind) { - diagnostics.push({ - file: this.event.file, + this.event.file.addDiagnostic({ ...DiagnosticMessages.enumValueMustBeType( enumValueKind.replace(/literal$/i, '').toLowerCase() ), @@ -84,7 +106,7 @@ export class BrsFileValidator { //default value missing } else { - diagnostics.push({ + this.event.file.addDiagnostic({ file: this.event.file, ...DiagnosticMessages.enumValueIsRequired( enumValueKind.replace(/literal$/i, '').toLowerCase() diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 6ef5dc2d9..024ee1f34 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -1,7 +1,7 @@ /* eslint-disable no-bitwise */ import type { Token, Identifier } from '../lexer/Token'; import { TokenKind } from '../lexer/TokenKind'; -import type { Block, CommentStatement, FunctionStatement } from './Statement'; +import type { Block, CommentStatement, FunctionStatement, Statement } from './Statement'; import type { Range } from 'vscode-languageserver'; import util from '../util'; import type { BrsTranspileState } from './BrsTranspileState'; @@ -33,6 +33,17 @@ export abstract class Expression { public visitMode = InternalWalkMode.visitExpressions; public abstract walk(visitor: WalkVisitor, options: WalkOptions); + /** + * The parent node for this expression. This is set dynamically during `onFileValidate`, and should not be set directly. + */ + public parent?: Statement | Expression; + + /** + * Get the closest symbol table for this node. Should be overridden in children that directly contain a symbol table + */ + public getSymbolTable(): SymbolTable { + return this.parent?.getSymbolTable(); + } } export class BinaryExpression extends Expression { @@ -155,6 +166,10 @@ export class FunctionExpression extends Expression implements TypedefProvider { public symbolTable: SymbolTable; + public getSymbolTable() { + return this.symbolTable; + } + /** * The type this function returns */ @@ -333,6 +348,18 @@ export class NamespacedVariableNameExpression extends Expression { } range: Range; + // @ts-expect-error override the property + public get parent() { + return this._parent; + } + public set parent(value) { + if (this.expression) { + this.expression.parent = value; + } + this._parent = value; + } + private _parent: Expression | Statement; + transpile(state: BrsTranspileState) { return [ state.sourceNode(this, this.getName(ParseMode.BrightScript)) @@ -386,6 +413,18 @@ export class DottedGetExpression extends Expression { public readonly range: Range; + // @ts-expect-error override the property + public get parent() { + return this._parent; + } + public set parent(value) { + if (this.obj) { + this.obj.parent = value; + } + this._parent = value; + } + private _parent: Expression | Statement; + transpile(state: BrsTranspileState) { //if the callee starts with a namespace name, transpile the name if (state.file.calleeStartsWithNamespace(this)) { @@ -784,7 +823,7 @@ export class VariableExpression extends Expression { public readonly range: Range; public getName(parseMode: ParseMode) { - return parseMode === ParseMode.BrightScript ? this.name.text : this.name.text; + return this.name.text; } transpile(state: BrsTranspileState) { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 576c4df83..c53f9115f 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -261,7 +261,7 @@ export class Parser { private body() { const parentAnnotations = this.enterAnnotationBlock(); - let body = new Body([]); + let body = new Body([], this.symbolTable); if (this.tokens.length > 0) { this.consumeStatementSeparators(true); diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 4c13a85d7..b6a56a297 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -42,6 +42,18 @@ export abstract class Statement { public visitMode = InternalWalkMode.visitStatements; public abstract walk(visitor: WalkVisitor, options: WalkOptions); + + /** + * The parent node for this statement. This is set dynamically during `onFileValidate`, and should not be set directly. + */ + public parent?: Statement | Expression; + + /** + * Get the closest symbol table for this node. Should be overridden in children that directly contain a symbol table + */ + public getSymbolTable(): SymbolTable { + return this.parent?.getSymbolTable(); + } } export class EmptyStatement extends Statement { @@ -67,11 +79,16 @@ export class EmptyStatement extends Statement { */ export class Body extends Statement implements TypedefProvider { constructor( - public statements: Statement[] = [] + public statements: Statement[] = [], + public symbolTable = new SymbolTable() ) { super(); } + public getSymbolTable() { + return this.symbolTable; + } + public get range() { return util.createRangeFromPositions( this.statements[0]?.range.start ?? Position.create(0, 0), @@ -1110,6 +1127,10 @@ export class NamespaceStatement extends Statement implements TypedefProvider { public symbolTable: SymbolTable; + public getSymbolTable() { + return this.symbolTable; + } + /** * The string name for this namespace */