bsc-plugin-auto-findnode
Advanced tools
Comparing version 0.1.1 to 1.0.0-alpha.41
@@ -10,2 +10,8 @@ # Changelog | ||
## [1.0.0-alpha.41](https://github.com/rokucommunity/bsc-plugin-auto-findnode/compare/v0.1.1...v1.0.0-alpha.41) - 2025-01-13 | ||
### Changed | ||
- upgrade to [brighterscript@1.0.0-alpha.41](https://github.com/rokucommunity/brighterscript/blob/release-1.0.0/CHANGELOG.md#100-alpha41---2024-10-20) | ||
## [0.1.1](https://github.com/rokucommunity/bsc-plugin-auto-findnode/compare/v0.1.0...v0.1.1) - 2025-01-09 | ||
@@ -12,0 +18,0 @@ ### Fixed |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.validateNodeWithIDInjection = exports.findNodeWithIDInjection = exports.findChildrenWithIDs = void 0; | ||
exports.validateNodeWithIDInjection = exports.findNodeWithIDInjection = exports.ensureEditor = exports.findChildrenWithIDs = void 0; | ||
const brighterscript_1 = require("brighterscript"); | ||
const SGTypes_1 = require("brighterscript/dist/parser/SGTypes"); | ||
function findChildrenWithIDs(children) { | ||
var _a, _b, _c, _d, _e; | ||
var _a, _b, _c, _d, _e, _f; | ||
let foundIDs = new Map(); | ||
for (const child of children !== null && children !== void 0 ? children : []) { | ||
if (child.id) { | ||
foundIDs.set(child.id, (_e = (_d = (_c = (_b = (_a = child.attributes) === null || _a === void 0 ? void 0 : _a.find) === null || _b === void 0 ? void 0 : _b.call(_a, x => x.key.text === 'id')) === null || _c === void 0 ? void 0 : _c.value) === null || _d === void 0 ? void 0 : _d.range) !== null && _e !== void 0 ? _e : brighterscript_1.util.createRange(0, 0, 0, 100)); | ||
foundIDs.set(child.id, (_e = (_d = (_c = (_b = (_a = child.attributes) === null || _a === void 0 ? void 0 : _a.find) === null || _b === void 0 ? void 0 : _b.call(_a, x => x.tokens.key.text === 'id')) === null || _c === void 0 ? void 0 : _c.tokens.value) === null || _d === void 0 ? void 0 : _d.location) !== null && _e !== void 0 ? _e : brighterscript_1.util.createLocation(0, 0, 0, 100, (_f = child.location) === null || _f === void 0 ? void 0 : _f.uri)); | ||
} | ||
const subChildren = findChildrenWithIDs(child.children); | ||
const subChildren = findChildrenWithIDs(child.elements); | ||
foundIDs = new Map([...foundIDs, ...subChildren]); | ||
@@ -19,8 +18,31 @@ } | ||
exports.findChildrenWithIDs = findChildrenWithIDs; | ||
function findNodeWithIDInjection(program, entries, editor, createdFiles) { | ||
/** | ||
* Find the first function called `init()` across all files in a scope | ||
*/ | ||
function findInitFunction(scope) { | ||
for (const file of scope.getOwnFiles()) { | ||
if ((0, brighterscript_1.isBrsFile)(file)) { | ||
const initFunction = file.ast.findChild(x => (0, brighterscript_1.isFunctionStatement)(x) && x.getName(brighterscript_1.ParseMode.BrighterScript).toLowerCase() === 'init'); | ||
if (initFunction) { | ||
return { | ||
initFunction: initFunction, | ||
file: file | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
function ensureEditor(file) { | ||
if (!file.editor) { | ||
file.editor = new brighterscript_1.Editor(); | ||
} | ||
return file.editor; | ||
} | ||
exports.ensureEditor = ensureEditor; | ||
function findNodeWithIDInjection(event, createdFiles) { | ||
var _a, _b, _c; | ||
for (const scope of program.getScopes()) { | ||
for (const scope of event.program.getScopes()) { | ||
if ((0, brighterscript_1.isXmlScope)(scope)) { | ||
const xmlFile = scope.xmlFile; | ||
const ids = findChildrenWithIDs((_c = (_b = (_a = xmlFile.parser.ast.component) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.children) !== null && _c !== void 0 ? _c : []); | ||
const ids = findChildrenWithIDs((_c = (_b = (_a = xmlFile.parser.ast.componentElement) === null || _a === void 0 ? void 0 : _a.childrenElement) === null || _b === void 0 ? void 0 : _b.elements) !== null && _c !== void 0 ? _c : []); | ||
//skip this xml file if there are no nodes with IDs in it | ||
@@ -39,3 +61,3 @@ if (ids.size === 0) { | ||
//add the assignments to the top of the init function | ||
editor.arrayUnshift(initFunctionInfo.initFunction.func.body.statements, ...brighterscript_1.Parser.parse(initFunctionText).ast.statements[0].func.body.statements); | ||
ensureEditor(initFunctionInfo.file).arrayUnshift(initFunctionInfo.initFunction.func.body.statements, ...brighterscript_1.Parser.parse(initFunctionText).ast.statements[0].func.body.statements); | ||
//we don't have an init function, create a new file and insert an empty init function into it | ||
@@ -45,12 +67,12 @@ } | ||
//get a unique filename for the new file | ||
const pkgPath = getUniqueFilename(xmlFile, program); | ||
const pkgPath = getUniqueFilename(xmlFile, event.program); | ||
//create and add the new file to the program | ||
const brsFileWithInit = program.setFile(pkgPath, initFunctionText); | ||
const brsFileWithInit = event.program.setFile(pkgPath, initFunctionText); | ||
createdFiles.push(brsFileWithInit); | ||
//since this is a build event, we need to add this file to the list to be built since it's new; | ||
event.files.push(brsFileWithInit); | ||
//import this file into the current xml file | ||
editor.arrayPush(xmlFile.parser.ast.component.scripts, new SGTypes_1.SGScript({ | ||
text: 'script' | ||
}, [ | ||
(0, brighterscript_1.createSGAttribute)('uri', brighterscript_1.util.sanitizePkgPath(brsFileWithInit.pkgPath)) | ||
])); | ||
ensureEditor(brsFileWithInit).arrayPush(xmlFile.parser.ast.componentElement.elements, (0, brighterscript_1.createSGScript)({ | ||
uri: brighterscript_1.util.sanitizePkgPath(brsFileWithInit.pkgPath) | ||
})); | ||
} | ||
@@ -66,3 +88,3 @@ } | ||
const xmlFile = scope.xmlFile; | ||
const ids = findChildrenWithIDs((_c = (_b = (_a = xmlFile.parser.ast.component) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.children) !== null && _c !== void 0 ? _c : []); | ||
const ids = findChildrenWithIDs((_c = (_b = (_a = xmlFile.parser.ast.componentElement) === null || _a === void 0 ? void 0 : _a.childrenElement) === null || _b === void 0 ? void 0 : _b.elements) !== null && _c !== void 0 ? _c : []); | ||
if (ids.size > 0) { | ||
@@ -74,14 +96,13 @@ const { initFunction, file: initFunctionFile } = (_d = findInitFunction(scope)) !== null && _d !== void 0 ? _d : {}; | ||
if ((0, brighterscript_1.isDottedGetExpression)(expression.callee) && | ||
expression.callee.name.text.toLocaleLowerCase() === 'findnode' && | ||
expression.callee.tokens.name.text.toLocaleLowerCase() === 'findnode' && | ||
(0, brighterscript_1.isDottedGetExpression)(expression.callee.obj) && | ||
expression.callee.obj.name.text.toLocaleLowerCase() === 'top' && | ||
expression.callee.obj.tokens.name.text.toLocaleLowerCase() === 'top' && | ||
(0, brighterscript_1.isVariableExpression)(expression.callee.obj.obj) && | ||
expression.callee.obj.obj.name.text.toLocaleLowerCase() === 'm' && | ||
expression.callee.obj.obj.tokens.name.text.toLocaleLowerCase() === 'm' && | ||
(0, brighterscript_1.isLiteralString)(expression.args[0])) { | ||
let id = expression.args[0].token.text.replace(/^"/, '').replace(/"$/, ''); | ||
let warningRange = ids.get(id); | ||
if (warningRange !== undefined) { | ||
initFunctionFile.diagnostics.push({ | ||
file: initFunctionFile, | ||
range: expression.range, | ||
let id = expression.args[0].tokens.value.text.replace(/^"/, '').replace(/"$/, ''); | ||
let warningLocation = ids.get(id); | ||
if (warningLocation !== undefined) { | ||
program.diagnostics.register({ | ||
location: expression.location, | ||
severity: brighterscript_1.DiagnosticSeverity.Warning, | ||
@@ -91,3 +112,3 @@ message: `Unnecessary call to 'm.top.findNode("${id}")'`, | ||
message: `In scope '${scope.name}'`, | ||
location: brighterscript_1.util.createLocation(brighterscript_1.util.pathToUri(xmlFile.srcPath), warningRange) | ||
location: warningLocation | ||
}] | ||
@@ -106,18 +127,2 @@ }); | ||
/** | ||
* Find the first function called `init()` across all files in a scope | ||
*/ | ||
function findInitFunction(scope) { | ||
for (const file of scope.getOwnFiles()) { | ||
if ((0, brighterscript_1.isBrsFile)(file)) { | ||
const initFunction = file.parser.references.functionStatementLookup.get('init'); | ||
if (initFunction) { | ||
return { | ||
initFunction: initFunction, | ||
file: file | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Get a pkgPath for a new brs file that will sit next to the given xml file. This is deterministic, | ||
@@ -124,0 +129,0 @@ * so if the file already exists, we'll append the next available number number to the end of the filename to make it unique. |
@@ -75,21 +75,46 @@ "use strict"; | ||
}); | ||
it('does not crash when child is missing id prop', () => { | ||
(0, chai_1.expect)(Object.entries((0, findNodes_1.findChildrenWithIDs)([{ | ||
id: 1, | ||
location: {} | ||
}]))).to.eql([]); | ||
}); | ||
}); | ||
describe('ensureEditor', () => { | ||
it('adds editor if not there', () => { | ||
(0, chai_1.expect)((0, findNodes_1.ensureEditor)({})).to.instanceof(brighterscript_1.Editor); | ||
}); | ||
it('returns existing editor if already there', () => { | ||
const editor = new brighterscript_1.Editor(); | ||
(0, chai_1.expect)((0, findNodes_1.ensureEditor)({ editor: editor })).to.equal(editor); | ||
}); | ||
}); | ||
describe('findNodeWithIDInjection', () => { | ||
it('does not crash when is missing children', () => { | ||
it('does not crash when is missing various parts of the XmlFile', () => { | ||
const file = program.setFile('components/ZombieKeyboard.xml', ` | ||
<component name="ZombieKeyboard"> | ||
<children></children> | ||
</component> | ||
`); | ||
delete file.parser.ast.component; | ||
(0, findNodes_1.findNodeWithIDInjection)(program, [], new brighterscript_1.AstEditor(), []); | ||
delete file.parser.ast.componentElement.childrenElement.elements; | ||
(0, findNodes_1.findNodeWithIDInjection)({ program: program, files: [] }, []); | ||
delete file.parser.ast.componentElement.childrenElement; | ||
(0, findNodes_1.findNodeWithIDInjection)({ program: program, files: [] }, []); | ||
delete file.parser.ast.componentElement; | ||
(0, findNodes_1.findNodeWithIDInjection)({ program: program, files: [] }, []); | ||
}); | ||
}); | ||
describe('validateNodeWithIDInjection', () => { | ||
it('does not crash when is missing children', () => { | ||
it('does not crash when is missing various parts of the XmlFile', () => { | ||
const file = program.setFile('components/ZombieKeyboard.xml', ` | ||
<component name="ZombieKeyboard"> | ||
<children></children> | ||
</component> | ||
`); | ||
delete file.parser.ast.component; | ||
delete file.parser.ast.componentElement.childrenElement.elements; | ||
(0, findNodes_1.validateNodeWithIDInjection)(program); | ||
delete file.parser.ast.componentElement.childrenElement; | ||
(0, findNodes_1.validateNodeWithIDInjection)(program); | ||
delete file.parser.ast.componentElement; | ||
(0, findNodes_1.validateNodeWithIDInjection)(program); | ||
}); | ||
@@ -144,3 +169,5 @@ it('does not crash on non-findnode calls', () => { | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard.xml`).toString())).to.equal((0, undent_1.default) ` | ||
@@ -163,3 +190,5 @@ <component name="ZombieKeyboard" extends="group"> | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal(`sub init()\n m.helloZombieText = m.top.findNode("helloZombieText")\nend sub`); | ||
@@ -169,7 +198,7 @@ //make sure the import to this new file is present in the xml file | ||
<component name="ZombieKeyboard" extends="group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
</component> | ||
@@ -189,3 +218,5 @@ `); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString())).to.equal((0, undent_1.default) ` | ||
@@ -199,7 +230,7 @@ sub init() | ||
<component name="ZombieKeyboard" extends="group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
</component> | ||
@@ -221,3 +252,5 @@ `); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) ` | ||
@@ -233,6 +266,6 @@ sub init() | ||
<component name="ZombieKeyboard" extends="Group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" /> | ||
</component> | ||
@@ -243,3 +276,5 @@ `); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) `'original contents`); | ||
@@ -265,4 +300,6 @@ (0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString()).to.equal((0, undent_1.default) ` | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) `'original contents`); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString())).to.equal((0, undent_1.default) `'original contents`); | ||
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString())).to.equal((0, undent_1.default) ` | ||
@@ -356,15 +393,15 @@ sub init() | ||
program.validate(); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation }))).to.eql([{ | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => ({ message: x.message, location: x.location, relatedInformation: x.relatedInformation }))).to.eql([{ | ||
message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, | ||
range: brighterscript_1.util.createRange(2, 36, 2, 69), | ||
location: brighterscript_1.util.createLocation(2, 36, 2, 69, (0, brighterscript_1.standardizePath) `${rootDir}/components/ZombieKeyboard.bs`), | ||
relatedInformation: [{ | ||
message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, | ||
location: brighterscript_1.util.createLocation(brighterscript_1.util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), brighterscript_1.util.createRange(4, 31, 4, 46)) | ||
location: brighterscript_1.util.createLocation(4, 31, 4, 46, `${rootDir}/components/ZombieKeyboard.xml`) | ||
}] | ||
}, { | ||
message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, | ||
range: brighterscript_1.util.createRange(3, 37, 3, 70), | ||
location: brighterscript_1.util.createLocation(3, 37, 3, 70, (0, brighterscript_1.standardizePath) `${rootDir}/components/ZombieKeyboard.bs`), | ||
relatedInformation: [{ | ||
message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, | ||
location: brighterscript_1.util.createLocation(brighterscript_1.util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), brighterscript_1.util.createRange(4, 31, 4, 46)) | ||
location: brighterscript_1.util.createLocation(4, 31, 4, 46, `${rootDir}/components/ZombieKeyboard.xml`) | ||
}] | ||
@@ -388,3 +425,3 @@ }]); | ||
program.validate(); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation }))).to.eql([]); | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => ({ message: x.message, location: x.location, relatedInformation: x.relatedInformation }))).to.eql([]); | ||
}); | ||
@@ -414,3 +451,5 @@ it('it works when you extend a component and found nodes are declared within their correct component', () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
yield program.transpile([], stagingDir); | ||
yield program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/BaseKeyboard-findnode.brs`).toString())).to.equal((0, undent_1.default) ` | ||
@@ -417,0 +456,0 @@ sub init() |
@@ -10,12 +10,12 @@ "use strict"; | ||
} | ||
beforeProgramValidate(program) { | ||
(0, findNodes_1.validateNodeWithIDInjection)(program); | ||
beforeProgramValidate(event) { | ||
(0, findNodes_1.validateNodeWithIDInjection)(event.program); | ||
} | ||
beforeProgramTranspile(program, entries, editor) { | ||
beforeBuildProgram(event) { | ||
this.createdFiles = []; | ||
(0, findNodes_1.findNodeWithIDInjection)(program, entries, editor, this.createdFiles); | ||
(0, findNodes_1.findNodeWithIDInjection)(event, this.createdFiles); | ||
} | ||
afterProgramTranspile(program) { | ||
afterBuildProgram(event) { | ||
for (const file of this.createdFiles) { | ||
program.removeFile(file.pkgPath); | ||
event.program.removeFile(file.pkgPath); | ||
} | ||
@@ -22,0 +22,0 @@ } |
{ | ||
"name": "bsc-plugin-auto-findnode", | ||
"version": "0.1.1", | ||
"version": "1.0.0-alpha.41", | ||
"description": "A BrighterScript plugin that auto-injects `m.top.findNode()` calls in your component `init()` functions", | ||
@@ -59,3 +59,3 @@ "main": "dist/index.js", | ||
"@typescript-eslint/parser": "^5.30.7", | ||
"brighterscript": "^0.65.16", | ||
"brighterscript": "^1.0.0-alpha.41", | ||
"chai": "^4.2.0", | ||
@@ -68,2 +68,3 @@ "coveralls-next": "^4.2.0", | ||
"eslint-plugin-no-only-tests": "^2.6.0", | ||
"fs-extra": "^11.2.0", | ||
"mocha": "^9.1.3", | ||
@@ -70,0 +71,0 @@ "nyc": "^15.1.0", |
import type { XmlFile } from 'brighterscript'; | ||
import { Program, util, standardizePath as s, AstEditor } from 'brighterscript'; | ||
import { Program, util, standardizePath as s, AstEditor, Editor } from 'brighterscript'; | ||
import { expect } from 'chai'; | ||
@@ -8,3 +8,3 @@ import { Plugin } from './Plugin'; | ||
import * as fsExtra from 'fs-extra'; | ||
import { findChildrenWithIDs, findNodeWithIDInjection, validateNodeWithIDInjection } from './findNodes'; | ||
import { ensureEditor, findChildrenWithIDs, findNodeWithIDInjection, validateNodeWithIDInjection } from './findNodes'; | ||
const tempDir = s`${__dirname}/../.tmp`; | ||
@@ -53,13 +53,40 @@ const rootDir = s`${tempDir}/rootDir`; | ||
}); | ||
it('does not crash when child is missing id prop', () => { | ||
expect( | ||
Object.entries(findChildrenWithIDs([{ | ||
id: 1, | ||
location: {} | ||
} as any])) | ||
).to.eql([]); | ||
}); | ||
}); | ||
describe('ensureEditor', () => { | ||
it('adds editor if not there', () => { | ||
expect(ensureEditor({} as any)).to.instanceof(Editor); | ||
}); | ||
it('returns existing editor if already there', () => { | ||
const editor = new Editor(); | ||
expect(ensureEditor({ editor: editor } as any)).to.equal(editor); | ||
}); | ||
}); | ||
describe('findNodeWithIDInjection', () => { | ||
it('does not crash when is missing children', () => { | ||
it('does not crash when is missing various parts of the XmlFile', () => { | ||
const file = program.setFile<XmlFile>('components/ZombieKeyboard.xml', ` | ||
<component name="ZombieKeyboard"> | ||
<children></children> | ||
</component> | ||
`); | ||
delete file.parser.ast.component; | ||
findNodeWithIDInjection(program, [], new AstEditor(), []); | ||
delete (file.parser.ast.componentElement!.childrenElement as any).elements; | ||
findNodeWithIDInjection({ program: program, files: [] } as any, []); | ||
delete (file.parser.ast.componentElement as any).childrenElement; | ||
findNodeWithIDInjection({ program: program, files: [] } as any, []); | ||
delete (file.parser.ast as any).componentElement; | ||
findNodeWithIDInjection({ program: program, files: [] } as any, []); | ||
}); | ||
@@ -69,10 +96,17 @@ }); | ||
describe('validateNodeWithIDInjection', () => { | ||
it('does not crash when is missing children', () => { | ||
it('does not crash when is missing various parts of the XmlFile', () => { | ||
const file = program.setFile<XmlFile>('components/ZombieKeyboard.xml', ` | ||
<component name="ZombieKeyboard"> | ||
<children></children> | ||
</component> | ||
`); | ||
delete file.parser.ast.component; | ||
delete (file.parser.ast.componentElement!.childrenElement as any).elements; | ||
validateNodeWithIDInjection(program); | ||
delete (file.parser.ast.componentElement as any).childrenElement; | ||
validateNodeWithIDInjection(program); | ||
delete (file.parser.ast as any).componentElement; | ||
validateNodeWithIDInjection(program); | ||
}); | ||
@@ -136,3 +170,5 @@ | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -162,3 +198,5 @@ expect( | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -174,7 +212,7 @@ expect( | ||
<component name="ZombieKeyboard" extends="group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
</component> | ||
@@ -196,3 +234,5 @@ `); | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -212,7 +252,7 @@ expect( | ||
<component name="ZombieKeyboard" extends="group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" /> | ||
<script type="text/brightscript" uri="pkg:/source/bslib.brs" /> | ||
</component> | ||
@@ -237,3 +277,5 @@ `); | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -254,6 +296,6 @@ expect( | ||
<component name="ZombieKeyboard" extends="Group"> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" /> | ||
<children> | ||
<label id="helloZombieText" /> | ||
</children> | ||
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" /> | ||
</component> | ||
@@ -265,3 +307,5 @@ `); | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -296,6 +340,8 @@ expect( | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
expect( | ||
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString() | ||
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()) | ||
).to.equal(undent`'original contents`); | ||
@@ -406,11 +452,11 @@ expect( | ||
expect( | ||
program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation })) | ||
program.getDiagnostics().map(x => ({ message: x.message, location: x.location, relatedInformation: x.relatedInformation })) | ||
).to.eql([{ | ||
message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, | ||
range: util.createRange(2, 36, 2, 69), | ||
location: util.createLocation(2, 36, 2, 69, s`${rootDir}/components/ZombieKeyboard.bs`), | ||
relatedInformation: [{ | ||
message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, | ||
location: util.createLocation( | ||
util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), | ||
util.createRange(4, 31, 4, 46) | ||
4, 31, 4, 46, | ||
`${rootDir}/components/ZombieKeyboard.xml` | ||
) | ||
@@ -420,8 +466,8 @@ }] | ||
message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, | ||
range: util.createRange(3, 37, 3, 70), | ||
location: util.createLocation(3, 37, 3, 70, s`${rootDir}/components/ZombieKeyboard.bs`), | ||
relatedInformation: [{ | ||
message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, | ||
location: util.createLocation( | ||
util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), | ||
util.createRange(4, 31, 4, 46) | ||
4, 31, 4, 46, | ||
`${rootDir}/components/ZombieKeyboard.xml` | ||
) | ||
@@ -451,3 +497,3 @@ }] | ||
expect( | ||
program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation })) | ||
program.getDiagnostics().map(x => ({ message: x.message, location: x.location, relatedInformation: x.relatedInformation })) | ||
).to.eql([]); | ||
@@ -482,3 +528,5 @@ }); | ||
expect(program.getDiagnostics().map(x => x.message)).to.eql([]); | ||
await program.transpile([], stagingDir); | ||
await program.build({ | ||
stagingDir: stagingDir | ||
}); | ||
@@ -493,3 +541,2 @@ expect( | ||
expect( | ||
@@ -496,0 +543,0 @@ undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard.brs`).toString()) |
@@ -1,12 +0,12 @@ | ||
import type { AstEditor, FunctionStatement, BrsFile, Program, BscFile, Range, TranspileObj, Scope, XmlFile } from 'brighterscript'; | ||
import { isBrsFile, Parser, isXmlScope, DiagnosticSeverity, createVisitor, WalkMode, isDottedGetExpression, isVariableExpression, isLiteralString, util, createSGAttribute } from 'brighterscript'; | ||
import type { FunctionStatement, BrsFile, Program, BscFile, Location, Scope, XmlFile, BeforeBuildProgramEvent } from 'brighterscript'; | ||
import { isBrsFile, Parser, isXmlScope, DiagnosticSeverity, createVisitor, WalkMode, isDottedGetExpression, isVariableExpression, isLiteralString, util, createSGAttribute, isFunctionStatement, ParseMode, Editor, createSGToken, createSGScript } from 'brighterscript'; | ||
import { SGScript, type SGNode } from 'brighterscript/dist/parser/SGTypes'; | ||
export function findChildrenWithIDs(children: Array<SGNode>): Map<string, Range> { | ||
let foundIDs = new Map<string, Range>(); | ||
export function findChildrenWithIDs(children: Array<SGNode>): Map<string, Location> { | ||
let foundIDs = new Map<string, Location>(); | ||
for (const child of children ?? []) { | ||
if (child.id) { | ||
foundIDs.set(child.id, child.attributes?.find?.(x => x.key.text === 'id')?.value?.range ?? util.createRange(0, 0, 0, 100)); | ||
foundIDs.set(child.id, child.attributes?.find?.(x => x.tokens.key.text === 'id')?.tokens.value?.location ?? util.createLocation(0, 0, 0, 100, child.location?.uri)); | ||
} | ||
const subChildren = findChildrenWithIDs(child.children); | ||
const subChildren = findChildrenWithIDs(child.elements); | ||
foundIDs = new Map([...foundIDs, ...subChildren]); | ||
@@ -17,7 +17,32 @@ } | ||
export function findNodeWithIDInjection(program: Program, entries: TranspileObj[], editor: AstEditor, createdFiles: BscFile[]) { | ||
for (const scope of program.getScopes()) { | ||
/** | ||
* Find the first function called `init()` across all files in a scope | ||
*/ | ||
function findInitFunction(scope: Scope): { file: BscFile; initFunction: FunctionStatement } | undefined { | ||
for (const file of scope.getOwnFiles()) { | ||
if (isBrsFile(file)) { | ||
const initFunction = file.ast.findChild<FunctionStatement>(x => isFunctionStatement(x) && x.getName(ParseMode.BrighterScript).toLowerCase() === 'init'); | ||
if (initFunction) { | ||
return { | ||
initFunction: initFunction, | ||
file: file | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
export function ensureEditor(file: BscFile) { | ||
if (!file.editor) { | ||
file.editor = new Editor(); | ||
} | ||
return file.editor; | ||
} | ||
export function findNodeWithIDInjection(event: BeforeBuildProgramEvent, createdFiles: BscFile[]) { | ||
for (const scope of event.program.getScopes()) { | ||
if (isXmlScope(scope)) { | ||
const xmlFile = scope.xmlFile; | ||
const ids = findChildrenWithIDs(xmlFile.parser.ast.component?.children?.children ?? []); | ||
const ids = findChildrenWithIDs(xmlFile.parser.ast.componentElement?.childrenElement?.elements ?? []); | ||
@@ -41,3 +66,3 @@ //skip this xml file if there are no nodes with IDs in it | ||
//add the assignments to the top of the init function | ||
editor.arrayUnshift( | ||
ensureEditor(initFunctionInfo.file).arrayUnshift( | ||
initFunctionInfo.initFunction.func.body.statements, | ||
@@ -50,14 +75,14 @@ ...(Parser.parse(initFunctionText).ast.statements[0] as FunctionStatement).func.body.statements | ||
//get a unique filename for the new file | ||
const pkgPath = getUniqueFilename(xmlFile, program); | ||
const pkgPath = getUniqueFilename(xmlFile, event.program); | ||
//create and add the new file to the program | ||
const brsFileWithInit = program.setFile<BrsFile>(pkgPath, initFunctionText); | ||
const brsFileWithInit = event.program.setFile<BrsFile>(pkgPath, initFunctionText); | ||
createdFiles.push(brsFileWithInit); | ||
//since this is a build event, we need to add this file to the list to be built since it's new; | ||
event.files.push(brsFileWithInit); | ||
//import this file into the current xml file | ||
editor.arrayPush(xmlFile.parser.ast.component!.scripts, new SGScript({ | ||
text: 'script' | ||
}, [ | ||
createSGAttribute('uri', util.sanitizePkgPath(brsFileWithInit.pkgPath)) | ||
])); | ||
ensureEditor(brsFileWithInit).arrayPush(xmlFile.parser.ast.componentElement!.elements, createSGScript({ | ||
uri: util.sanitizePkgPath(brsFileWithInit.pkgPath) | ||
})); | ||
} | ||
@@ -72,3 +97,3 @@ } | ||
const xmlFile = scope.xmlFile; | ||
const ids = findChildrenWithIDs(xmlFile.parser.ast.component?.children?.children ?? []); | ||
const ids = findChildrenWithIDs(xmlFile.parser.ast.componentElement?.childrenElement?.elements ?? []); | ||
if (ids.size > 0) { | ||
@@ -82,15 +107,14 @@ const { initFunction, file: initFunctionFile } = findInitFunction(scope) ?? {}; | ||
isDottedGetExpression(expression.callee) && | ||
expression.callee.name.text.toLocaleLowerCase() === 'findnode' && | ||
expression.callee.tokens.name.text.toLocaleLowerCase() === 'findnode' && | ||
isDottedGetExpression(expression.callee.obj) && | ||
expression.callee.obj.name.text.toLocaleLowerCase() === 'top' && | ||
expression.callee.obj.tokens.name.text.toLocaleLowerCase() === 'top' && | ||
isVariableExpression(expression.callee.obj.obj) && | ||
expression.callee.obj.obj.name.text.toLocaleLowerCase() === 'm' && | ||
expression.callee.obj.obj.tokens.name.text.toLocaleLowerCase() === 'm' && | ||
isLiteralString(expression.args[0]) | ||
) { | ||
let id = expression.args[0].token.text.replace(/^"/, '').replace(/"$/, ''); | ||
let warningRange = ids.get(id); | ||
if (warningRange !== undefined) { | ||
initFunctionFile!.diagnostics.push({ | ||
file: initFunctionFile!, | ||
range: expression.range, | ||
let id = expression.args[0].tokens.value.text.replace(/^"/, '').replace(/"$/, ''); | ||
let warningLocation = ids.get(id); | ||
if (warningLocation !== undefined) { | ||
program.diagnostics.register({ | ||
location: expression.location!, | ||
severity: DiagnosticSeverity.Warning, | ||
@@ -100,6 +124,3 @@ message: `Unnecessary call to 'm.top.findNode("${id}")'`, | ||
message: `In scope '${scope.name}'`, | ||
location: util.createLocation( | ||
util.pathToUri(xmlFile.srcPath), | ||
warningRange | ||
) | ||
location: warningLocation | ||
}] | ||
@@ -118,19 +139,2 @@ }); | ||
/** | ||
* Find the first function called `init()` across all files in a scope | ||
*/ | ||
function findInitFunction(scope: Scope): { file: BscFile; initFunction: FunctionStatement } | undefined { | ||
for (const file of scope.getOwnFiles()) { | ||
if (isBrsFile(file)) { | ||
const initFunction = file.parser.references.functionStatementLookup.get('init'); | ||
if (initFunction) { | ||
return { | ||
initFunction: initFunction, | ||
file: file | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Get a pkgPath for a new brs file that will sit next to the given xml file. This is deterministic, | ||
@@ -137,0 +141,0 @@ * so if the file already exists, we'll append the next available number number to the end of the filename to make it unique. |
@@ -1,2 +0,2 @@ | ||
import type { AstEditor, BeforeFileValidateEvent, BscFile, CompilerPlugin, PluginHandler, Program, TranspileObj } from 'brighterscript'; | ||
import type { BscFile, CompilerPlugin, BeforeProgramValidateEvent, BeforeBuildProgramEvent, AfterBuildProgramEvent } from 'brighterscript'; | ||
import { findNodeWithIDInjection, validateNodeWithIDInjection } from './findNodes'; | ||
@@ -9,16 +9,16 @@ | ||
beforeProgramValidate(program: Program) { | ||
validateNodeWithIDInjection(program); | ||
beforeProgramValidate(event: BeforeProgramValidateEvent) { | ||
validateNodeWithIDInjection(event.program); | ||
} | ||
beforeProgramTranspile(program: Program, entries: TranspileObj[], editor: AstEditor) { | ||
beforeBuildProgram(event: BeforeBuildProgramEvent) { | ||
this.createdFiles = []; | ||
findNodeWithIDInjection(program, entries, editor, this.createdFiles); | ||
findNodeWithIDInjection(event, this.createdFiles); | ||
} | ||
afterProgramTranspile(program: Program) { | ||
afterBuildProgram(event: AfterBuildProgramEvent) { | ||
for (const file of this.createdFiles as BscFile[]) { | ||
program.removeFile(file.pkgPath); | ||
event.program.removeFile(file.pkgPath!); | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
197595
1558
22