diff --git a/src/Program.ts b/src/Program.ts index 94e32b6ed..bde2bffbe 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -395,10 +395,27 @@ export class Program { this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => { brsFile.parse(sourceObj.source); }); + + //notify plugins that this file has finished parsing + this.plugins.emit('afterFileParse', brsFile); + file = brsFile; brsFile.attachDependencyGraph(this.dependencyGraph); + this.plugins.emit('beforeFileValidate', { + program: this, + file: file + }); + + file.validate(); + + //emit an event to allow plugins to contribute to the file validation process + this.plugins.emit('onFileValidate', { + program: this, + file: file + }); + this.plugins.emit('afterFileValidate', brsFile); } else if ( //is xml file @@ -415,10 +432,14 @@ export class Program { source: fileContents }; this.plugins.emit('beforeFileParse', sourceObj); + this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => { xmlFile.parse(sourceObj.source); }); + //notify plugins that this file has finished parsing + this.plugins.emit('afterFileParse', xmlFile); + file = xmlFile; //create a new scope for this xml file @@ -428,6 +449,21 @@ export class Program { //register this compoent now that we have parsed it and know its component name this.registerComponent(xmlFile, scope); + + //emit an event before starting to validate this file + this.plugins.emit('beforeFileValidate', { + file: file, + program: this + }); + + xmlFile.validate(); + + //emit an event to allow plugins to contribute to the file validation process + this.plugins.emit('onFileValidate', { + file: xmlFile, + program: this + }); + this.plugins.emit('afterFileValidate', xmlFile); } else { //TODO do we actually need to implement this? Figure out how to handle img paths diff --git a/src/astUtils/visitors.spec.ts b/src/astUtils/visitors.spec.ts index 83dd056ea..b37cf4547 100644 --- a/src/astUtils/visitors.spec.ts +++ b/src/astUtils/visitors.spec.ts @@ -4,7 +4,7 @@ import { CancellationTokenSource, Range } from 'vscode-languageserver'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { Program } from '../Program'; -import { BrsFile } from '../files/BrsFile'; +import type { BrsFile } from '../files/BrsFile'; import type { Statement } from '../parser/Statement'; import { PrintStatement, Block, ReturnStatement } from '../parser/Statement'; import type { Expression } from '../parser/Expression'; @@ -17,7 +17,6 @@ import { createStackedVisitor } from './stackedVisitor'; describe('astUtils visitors', () => { const rootDir = process.cwd(); let program: Program; - let file: BrsFile; const PRINTS_SRC = ` sub Main() @@ -76,7 +75,6 @@ describe('astUtils visitors', () => { beforeEach(() => { program = new Program({ rootDir: rootDir }); - file = new BrsFile('abs.bs', 'rel.bs', program); }); afterEach(() => { program.dispose(); @@ -102,9 +100,9 @@ describe('astUtils visitors', () => { const walker = functionsWalker(visitor); program.plugins.add({ name: 'walker', - afterFileParse: () => walker(file) + afterFileParse: file => walker(file as BrsFile) }); - file.parse(PRINTS_SRC); + program.addOrReplaceFile('source/main.brs', PRINTS_SRC); expect(actual).to.deep.equal([ 'Block:0', // Main sub body 'PrintStatement:1', // print 1 @@ -139,9 +137,9 @@ describe('astUtils visitors', () => { const walker = functionsWalker(s => actual.push(s.constructor.name), cancel.token); program.plugins.add({ name: 'walker', - afterFileParse: () => walker(file) + afterFileParse: file => walker(file as BrsFile) }); - file.parse(PRINTS_SRC); + program.addOrReplaceFile('source/main.brs', PRINTS_SRC); expect(actual).to.deep.equal([ 'Block', // Main sub body 'PrintStatement', // print 1 @@ -184,9 +182,9 @@ describe('astUtils visitors', () => { }, cancel.token); program.plugins.add({ name: 'walker', - afterFileParse: () => walker(file) + afterFileParse: file => walker(file as BrsFile) }); - file.parse(PRINTS_SRC); + program.addOrReplaceFile('source/main.brs', PRINTS_SRC); expect(actual).to.deep.equal([ 'Block', // Main sub body 'PrintStatement', // print 1 @@ -263,10 +261,10 @@ describe('astUtils visitors', () => { }); program.plugins.add({ name: 'walker', - afterFileParse: () => walker(file) + afterFileParse: (file) => walker(file as BrsFile) }); - file.parse(EXPRESSIONS_SRC); + program.addOrReplaceFile('source/main.brs', EXPRESSIONS_SRC); expect(actual).to.deep.equal([ //The comment statement is weird because it can't be both a statement and expression, but is treated that way. Just ignore it for now until we refactor comments. //'CommentStatement:1:CommentStatement', // ' diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 3c31eb557..e497665aa 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -2293,13 +2293,12 @@ describe('BrsFile', () => { name: 'transform callback', afterFileParse: onParsed }); - const file = new BrsFile(`absolute_path/file${ext}`, `relative_path/file${ext}`, program); - expect(file.extension).to.equal(ext); - file.parse(` + file = program.addOrReplaceFile({ src: `absolute_path/file${ext}`, dest: `relative_path/file${ext}` }, ` sub Sum() print "hello world" end sub `); + expect(file.extension).to.equal(ext); return file; } diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 8d0a59df8..7aae6ca23 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -268,28 +268,14 @@ export class BrsFile { ...this._parser.diagnostics as BsDiagnostic[] ); - //notify AST ready - this.program.plugins.emit('afterFileParse', this); - //extract all callables from this file this.findCallables(); //find all places where a sub/function is being called this.findFunctionCalls(); - //emit an event before starting to validate this file - this.program.plugins.emit('beforeFileValidate', { - file: this, - program: this.program - }); - - //emit an event to allow plugins to contribute to the file validation process - this.program.plugins.emit('onFileValidate', { - file: this, - program: this.program - }); - - this.findAndValidateImportAndImportStatements(); + //register all import statements for use in the rest of the program + this.registerImports(); //attach this file to every diagnostic for (let diagnostic of this.diagnostics) { @@ -305,9 +291,29 @@ export class BrsFile { } } - public findAndValidateImportAndImportStatements() { - let topOfFileIncludeStatements = [] as Array; + private registerImports() { + for (const statement of this.parser?.references?.importStatements ?? []) { + //register import statements + if (isImportStatement(statement) && statement.filePathToken) { + this.ownScriptImports.push({ + filePathRange: statement.filePathToken.range, + pkgPath: util.getPkgPathFromTarget(this.pkgPath, statement.filePath), + sourceFile: this, + text: statement.filePathToken?.text + }); + } + } + } + + public validate() { + //only validate the file if it was actually parsed (skip files containing typedefs) + if (!this.hasTypedef) { + this.validateImportStatements(); + } + } + private validateImportStatements() { + let topOfFileIncludeStatements = [] as Array; for (let stmt of this.ast.statements) { //skip comments if (isCommentStatement(stmt)) { @@ -327,16 +333,6 @@ export class BrsFile { ...this._parser.references.importStatements ]; for (let result of statements) { - //register import statements - if (isImportStatement(result) && result.filePathToken) { - this.ownScriptImports.push({ - filePathRange: result.filePathToken.range, - pkgPath: util.getPkgPathFromTarget(this.pkgPath, result.filePath), - sourceFile: this, - text: result.filePathToken?.text - }); - } - //if this statement is not one of the top-of-file statements, //then add a diagnostic explaining that it is invalid if (!topOfFileIncludeStatements.includes(result)) { diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts index c431d4204..d05fbfec8 100644 --- a/src/files/XmlFile.spec.ts +++ b/src/files/XmlFile.spec.ts @@ -13,6 +13,7 @@ import { standardizePath as s } from '../util'; import { expectDiagnostics, expectZeroDiagnostics, getTestTranspile, trim, trimMap } from '../testHelpers.spec'; import { ProgramBuilder } from '../ProgramBuilder'; import { LogLevel } from '../Logger'; +import { isXmlFile } from '../astUtils/reflection'; describe('XmlFile', () => { const tempDir = s`${process.cwd()}/.tmp`; @@ -40,17 +41,17 @@ describe('XmlFile', () => { describe('parse', () => { it('allows modifying the parsed XML model', () => { const expected = 'OtherName'; - file = new XmlFile('abs', 'rel', program); program.plugins.add({ name: 'allows modifying the parsed XML model', - afterFileParse: () => { - file.parser.ast.root.attributes[0].value.text = expected; + afterFileParse: (file) => { + if (isXmlFile(file) && file.parser.ast.root?.attributes?.[0]?.value) { + file.parser.ast.root.attributes[0].value.text = expected; + } } }); - file.parse(trim` + file = program.addOrReplaceFile('components/ChildScene.xml', trim` -