diff --git a/.eslintrc.js b/.eslintrc.js index 53814ef..d3e3e34 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,8 @@ module.exports = { plugins: [ '@typescript-eslint', 'no-only-tests', - 'jsdoc' + 'jsdoc', + 'import' ], extends: [ 'eslint:all', @@ -133,6 +134,8 @@ module.exports = { 'max-lines-per-function': 'off', 'max-params': 'off', 'max-statements': 'off', + 'import/no-duplicates': ['error'], + 'no-duplicate-imports': 'off', 'no-only-tests/no-only-tests': 'error', 'multiline-comment-style': 'off', 'multiline-ternary': 'off', diff --git a/.vscode/settings.json b/.vscode/settings.json index 3d00bdc..c80ab65 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "editor.tabSize": 4, "editor.insertSpaces": true, "editor.detectIndentation": false, + "editor.formatOnSave": true, "cSpell.words": [ "brighterscript", "findnode", diff --git a/package-lock.json b/package-lock.json index 4c2b9f7..07ea392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -367,9 +367,9 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.31.0.tgz", - "integrity": "sha512-tc1/iuQcnaiSIUVad72PBierDFpsxdUHtEF/OrfqvM1CBAsIoMP51j52jTMb3dXriwhieTo289InzZj72jL3EQ==", + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", + "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", "dev": true, "dependencies": { "comment-parser": "1.3.1", @@ -377,7 +377,7 @@ "jsdoc-type-pratt-parser": "~3.1.0" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18" + "node": "^14 || ^16 || ^17 || ^18 || ^19" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2455,21 +2455,21 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "39.3.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.3.6.tgz", - "integrity": "sha512-R6dZ4t83qPdMhIOGr7g2QII2pwCjYyKP+z0tPOfO1bbAbQyKC20Y2Rd6z1te86Lq3T7uM8bNo+VD9YFpE8HU/g==", + "version": "39.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz", + "integrity": "sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.31.0", + "@es-joy/jsdoccomment": "~0.36.1", "comment-parser": "1.3.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.4.0", - "semver": "^7.3.7", + "semver": "^7.3.8", "spdx-expression-parse": "^3.0.1" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18" + "node": "^14 || ^16 || ^17 || ^18 || ^19" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" @@ -7037,9 +7037,9 @@ } }, "@es-joy/jsdoccomment": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.31.0.tgz", - "integrity": "sha512-tc1/iuQcnaiSIUVad72PBierDFpsxdUHtEF/OrfqvM1CBAsIoMP51j52jTMb3dXriwhieTo289InzZj72jL3EQ==", + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", + "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", "dev": true, "requires": { "comment-parser": "1.3.1", @@ -8675,17 +8675,17 @@ } }, "eslint-plugin-jsdoc": { - "version": "39.3.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.3.6.tgz", - "integrity": "sha512-R6dZ4t83qPdMhIOGr7g2QII2pwCjYyKP+z0tPOfO1bbAbQyKC20Y2Rd6z1te86Lq3T7uM8bNo+VD9YFpE8HU/g==", + "version": "39.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz", + "integrity": "sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw==", "dev": true, "requires": { - "@es-joy/jsdoccomment": "~0.31.0", + "@es-joy/jsdoccomment": "~0.36.1", "comment-parser": "1.3.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.4.0", - "semver": "^7.3.7", + "semver": "^7.3.8", "spdx-expression-parse": "^3.0.1" }, "dependencies": { diff --git a/src/Plugin.ts b/src/Plugin.ts index 03cc174..95ca604 100644 --- a/src/Plugin.ts +++ b/src/Plugin.ts @@ -20,4 +20,4 @@ export class Plugin implements CompilerPlugin { program.removeFile(file.pkgPath); } } -} +} \ No newline at end of file diff --git a/src/findNodes.spec.ts b/src/findNodes.spec.ts index 1d82491..72195fd 100644 --- a/src/findNodes.spec.ts +++ b/src/findNodes.spec.ts @@ -6,9 +6,10 @@ import undent from 'undent'; describe('findnode', () => { let program: Program; + const rootDir = path.join(__dirname, '../.tmp'); beforeEach(() => { - program = new Program({}); + program = new Program({ rootDir: rootDir }); program.plugins.add(new Plugin()); }); @@ -167,13 +168,27 @@ describe('findnode', () => { program.validate(); expect( - program.getDiagnostics().map(x => ({ message: x.message, range: x.range })) + program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation })) ).to.eql([{ message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, - range: util.createRange(2, 36, 2, 69) + range: util.createRange(2, 36, 2, 69), + relatedInformation: [{ + message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, + location: util.createLocation( + util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), + util.createRange(4, 31, 4, 46) + ) + }] }, { message: `Unnecessary call to 'm.top.findNode("helloZombieText")'`, - range: util.createRange(3, 37, 3, 70) + range: util.createRange(3, 37, 3, 70), + relatedInformation: [{ + message: `In scope 'components${path.sep}ZombieKeyboard.xml'`, + location: util.createLocation( + util.pathToUri(`${rootDir}/components/ZombieKeyboard.xml`), + util.createRange(4, 31, 4, 46) + ) + }] }]); }); diff --git a/src/findNodes.ts b/src/findNodes.ts index 65125b4..b066510 100644 --- a/src/findNodes.ts +++ b/src/findNodes.ts @@ -1,17 +1,16 @@ -import type { AstEditor, FunctionStatement, BrsFile, Program, TranspileObj, BscFile, Statement } from 'brighterscript'; -// eslint-disable-next-line no-duplicate-imports +import type { AstEditor, FunctionStatement, BrsFile, Program, TranspileObj, BscFile, Statement, Range } from 'brighterscript'; import { isBrsFile, Parser, isXmlScope, DiagnosticSeverity, createVisitor, WalkMode, isDottedGetExpression, isVariableExpression, isLiteralString, util } from 'brighterscript'; import type { SGNode } from 'brighterscript/dist/parser/SGTypes'; -function findChildrenWithIDs(children: Array): string[] { - let foundIDs: string[] = []; +function findChildrenWithIDs(children: Array): Map { + let foundIDs = new Map(); if (children) { children.forEach(child => { if (child.id) { - foundIDs.push(child.id); + foundIDs.set(child.id, child.attributes.find(x => x.key.text === 'id')?.value?.range ?? util.createRange(0, 0, 0, 100)); } const subChildren = findChildrenWithIDs(child.children); - foundIDs = foundIDs.concat(subChildren); + foundIDs = new Map([...foundIDs, ...subChildren]); }); } return foundIDs; @@ -22,7 +21,7 @@ export function findNodeWithIDInjection(program: Program, entries: TranspileObj[ if (isXmlScope(scope)) { const xmlFile = scope.xmlFile; const ids = findChildrenWithIDs(xmlFile.parser.ast.component?.children?.children ?? []); - if (ids.length > 0) { + if (ids.size > 0) { const scopeFiles: BscFile[] = scope.getOwnFiles(); //find an init function from all the scope's files @@ -55,12 +54,12 @@ export function findNodeWithIDInjection(program: Program, entries: TranspileObj[ if (initFunction) { //add m variables for every xml component that has an id // eslint-disable-next-line max-statements-per-line, @typescript-eslint/brace-style - const assignments = ids.map(id => { return `m.${id} = m.top.findNode("${id}")`; }).join('\n'); + const assignments = Array.from(ids).map(([id, range]) => { return `m.${id} = m.top.findNode("${id}")`; }).join('\n'); const statements = (Parser.parse(` - sub temp() - ${assignments} - end sub - `).statements[0] as FunctionStatement).func.body.statements; + sub temp() + ${assignments} + end sub + `).statements[0] as FunctionStatement).func.body.statements; //add the assignments to the top of the init function editor.arrayUnshift(initFunction.func.body.statements, ...statements); } @@ -74,11 +73,11 @@ export function validateNodeWithIDInjection(program: Program) { if (isXmlScope(scope)) { const xmlFile = scope.xmlFile; const ids = findChildrenWithIDs(xmlFile.parser.ast.component?.children?.children ?? []); - if (ids.length > 0) { + if (ids.size > 0) { const scopeFiles: BscFile[] = scope.getOwnFiles(); let initFunction: FunctionStatement | undefined; - let initFunctionFile: BrsFile | undefined; + let initFunctionFile: BscFile | undefined; for (const file of scopeFiles) { if (isBrsFile(file)) { @@ -103,7 +102,8 @@ export function validateNodeWithIDInjection(program: Program) { isLiteralString(expression.args[0]) ) { let id = expression.args[0].token.text.replace(/^"/, '').replace(/"$/, ''); - if (id && ids.includes(id)) { + let warningRange = ids.get(id); + if (warningRange !== undefined) { initFunctionFile!.diagnostics.push({ file: initFunctionFile!, range: expression.range, @@ -113,7 +113,7 @@ export function validateNodeWithIDInjection(program: Program) { message: `In scope '${scope.name}'`, location: util.createLocation( util.pathToUri(xmlFile.srcPath), - util.createRange(0, 0, 0, 100) + warningRange ) }] });