diff --git a/docs/bsconfig.md b/docs/bsconfig.md index 01594bca5..b899430de 100644 --- a/docs/bsconfig.md +++ b/docs/bsconfig.md @@ -46,7 +46,7 @@ BrighterScript only: will automatically import a script at transpile-time for a Type: `string` -Override the destination directory for the bslib.brs file. Use this if you want to customize where the bslib.brs file is located in the staging directory. Note that using a location outside of `source` will break scripts inside `source` that depend on bslib.brs. +Override the destination directory for the bslib.brs file. Use this if you want to customize where the bslib.brs file is located in the staging directory. Note that using a location outside of `source` will break scripts inside `source` that depend on bslib.brs. Defaults to `source`. @@ -70,34 +70,60 @@ If `true`, after a successful build, the project will be deployed to the Roku sp ## `diagnosticFilters` -Type: `Array; codes?: Array}` A list of filters used to hide diagnostics. -- A `string` value should be a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. Any file matching this pattern will have all diagnostics supressed. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. - - A file glob may be prefixed with `!` to make it a negative pattern which "un-ignores" the files it matches. (See examples below). -- A `number` value should be a diagnostic code. This will supress all diagnostics with that code for the whole project. -- An object can also be provided to filter specific diagnostic codes for a file pattern. For example, +- A `string` value should be a diagnostic code. This will supress all diagnostics with that code for the whole project. For example: - ```jsonc - "diagnosticFilters": [{ - "src": "vendor/**/*", - "codes": [1000, 1011] //ignore these specific codes from vendor libraries - }] - ``` + ```jsonc + "diagnosticFilters": [ + "cannot-find-name", + "local-var-function-shadow", + "mismatch-argument-count" + ] + ``` + +- An object can also be provided to filter specific diagnostic codes for a file pattern. If no `files` property is included, any diagnostic that matches the values in the `codes` will b e suppressed. If a `string` if given for the `files` property, it is treated as a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. For example, + + ```jsonc + "diagnosticFilters": [{ + "files": "vendor/**/*", + "codes": ["cannot-find-name", "mismatch-argument-count"] //ignore these specific codes from vendor libraries + }] + ``` + +- If an object is provided, the `files` property could also be an array, providing either a series of globs, or a specific set of files that match _either_ their `src` or `dest` paths. For example, + + ```jsonc + "diagnosticFilters": [{ + "files": [ + "vendor/**/*", // all vendor files will be suppressed + { "src": "themes/theme1/**/*"}, // all files coming from `themes/theme1/` will be suppressed + { "dest": "source/common/**/*"}, // all files from `source/common/` will be suppressed + ] + "codes": ["cannot-find-name", "mismatch-argument-count"] //ignore these specific codes + }] + ``` + +- A file glob may be prefixed with `!` to make it a negative pattern which "un-ignores" the files it matches. (See examples below). Defaults to `undefined`. If a child bsconfig extends from a parent bsconfig, and both bsconfigs specify `diagnosticFilters`, the parent bsconfig's `diagnosticFilters` field will be completely overwritten. + +**Note:** In Brighterscript v0, all diagnostics used a numerical code. Using those legacy codes will still work, but is *deprecated*. Therefore, it is possible to use `number` values instead of string codes. Using a number (or a string representation of a number) matches all diagnostic codes that have that legacy code. For example, an entry of `1234` (or `"1234"`) would suppress any diagnostic with legacy code `1234`. + + ### Negative patterns in `diagnosticFilters` A negative pattern can be used to un-ignore some files or codes which were previously ignored. For example, ```jsonc "diagnosticFilters": [ - { "src": "vendor/**/*" }, //ignore all codes from vendor libraries - { "src": "!vendor/unreliable/**/*" } //EXCEPT do show errors from this one specific library + { "files": "vendor/**/*" }, //ignore all codes from vendor libraries + { "files": "!vendor/unreliable/**/*" } //EXCEPT do show errors from this one specific library ] ``` @@ -105,8 +131,8 @@ A specific error code can be unignored in multiple places by using a pattern whi ```jsonc "diagnosticFilters": [ - { "src": "vendor/**/*" }, //ignore all errors from vendor libraries - { "src": "!*/**/*", "codes": [1000] } //EXCEPT do show this particular code everywhere + { "files": "vendor/**/*" }, //ignore all errors from vendor libraries + { "files": "!*/**/*", "codes": ["name-collision"] } //EXCEPT do show this particular code everywhere ] ``` @@ -126,7 +152,7 @@ A map of error codes and severity levels that will override diagnostics' severit ```jsonc "diagnosticSeverityOverrides": { - "1011": "error", //raise a warning to an error + "local-var-function-shadow": "error", //raise a warning to an error "LINT1001": "warn" //oops we have lots of those to fix... later } ``` @@ -156,47 +182,51 @@ Child config properties completely replace parent config properties. For example All relative paths found in the configuration file will be resolved relative to the configuration file they originated in. ### Optional `extends` and `project` + There are situations where you want to store some compiler settings in a config file, but not fail if that config file doesn't exist. To do this, you can denote that your [`extends`](#extends) or [`project`](#project) path is optional by prefixing it with a question mark (`?`). For example: - **bsconfig.json** `extends` - ```json - { - "extends": "?path/to/optional/bsconfig.json" - } - ``` + ```json + { + "extends": "?path/to/optional/bsconfig.json" + } + ``` - CLI "extends" - ``` - bsc --extends "?path/to/optional/bsconfig.json" - ``` + + ``` + bsc --extends "?path/to/optional/bsconfig.json" + ``` - CLI `project` argument - ``` - bsc --project "?path/to/optional/bsconfig.json" - ``` + ``` + bsc --project "?path/to/optional/bsconfig.json" + ``` - Node.js API `extends` - ``` - var programBuilder = new ProgramBuilder({ - "extends": "?path/to/optional/bsconfig.json" - }); - ``` + ``` + var programBuilder = new ProgramBuilder({ + "extends": "?path/to/optional/bsconfig.json" + }); + ``` - Node.js API `project` - ``` - var programBuilder = new ProgramBuilder({ - "project": "?path/to/optional/bsconfig.json" - }); - ``` + ``` + var programBuilder = new ProgramBuilder({ + "project": "?path/to/optional/bsconfig.json" + }); + ``` ## `files` Type: + ```typescript Array< - string | - string[] | - { - src: string | string[], - dest: string - }> + | string + | string[] + | { + src: string | string[]; + dest: string; + } +>; ``` The files array is how you specify what files are included in your project. Any strings found in the files array must be relative to rootDir, and are used as include filters, meaning that if a file matches the pattern, it is included. @@ -205,12 +235,7 @@ For most standard projects, the default files array should work just fine: ```jsonc { - "files": [ - "source/**/*", - "components/**/*", - "images/**/*", - "manifest" - ] + "files": ["source/**/*", "components/**/*", "images/**/*", "manifest"] } ``` @@ -239,10 +264,7 @@ You can exclude files from the output by prefixing your file patterns with "!". ```jsonc { - "files": [ - "source/**/*", - "!source/some/unwanted/file.brs" - ] + "files": ["source/**/*", "!source/some/unwanted/file.brs"] } ``` @@ -256,13 +278,13 @@ Patterns may not reference files outside of [`rootDir`](#rootdir) unless the `{ ```jsonc { - "rootDir": "C:/projects/CatVideoPlayer", - "files": [ - "source/main.brs", + "rootDir": "C:/projects/CatVideoPlayer", + "files": [ + "source/main.brs", - //NOT allowed because it navigates outside the rootDir - "../common/promise.brs" - ] + //NOT allowed because it navigates outside the rootDir + "../common/promise.brs" + ] } ``` @@ -280,17 +302,17 @@ The object structure is as follows: ```typescript { - /** - * A glob pattern string or file path, or an array of glob pattern strings and/or file paths. - * These can be relative paths or absolute paths. - * All non-absolute paths are resolved relative to the rootDir - */ - src: Array; - /** - * The relative path to the location in the output folder where the files should be placed, - * relative to the root of the output folder - */ - dest: string | undefined + /** + * A glob pattern string or file path, or an array of glob pattern strings and/or file paths. + * These can be relative paths or absolute paths. + * All non-absolute paths are resolved relative to the rootDir + */ + src: Array; + /** + * The relative path to the location in the output folder where the files should be placed, + * relative to the root of the output folder + */ + dest: string | undefined; } ``` @@ -318,14 +340,14 @@ An example of combining regular and advanced file patterns: ```jsonc { - "rootDir": "C:/projects/CatVideoPlayer", - "files": [ - "source/main.brs", - { - "src": "../common/promise.brs", - "dest": "source/common" - } - ] + "rootDir": "C:/projects/CatVideoPlayer", + "files": [ + "source/main.brs", + { + "src": "../common/promise.brs", + "dest": "source/common" + } + ] } ``` @@ -337,15 +359,15 @@ For example, if you have a base project and a child project that wants to overri ```jsonc { - "files": [ - { - //copy all files from the base project - "src": "../BaseProject/**/*" - }, - // Override "../BaseProject/themes/theme.brs" - // with "${rootDir}/themes/theme.brs" - "themes/theme.brs" - ] + "files": [ + { + //copy all files from the base project + "src": "../BaseProject/**/*" + }, + // Override "../BaseProject/themes/theme.brs" + // with "${rootDir}/themes/theme.brs" + "themes/theme.brs" + ] } ``` diff --git a/src/BsConfig.ts b/src/BsConfig.ts index 83786b2ea..6777ef59e 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -128,7 +128,7 @@ export interface BsConfig { /** * A list of filters used to exclude diagnostics from the output */ - diagnosticFilters?: Array; + diagnosticFilters?: Array; codes?: Array }>; /** * Specify what diagnostic types should be printed to the console. Defaults to 'warn' diff --git a/src/DiagnosticFilterer.spec.ts b/src/DiagnosticFilterer.spec.ts index f76bd1a4f..cec005396 100644 --- a/src/DiagnosticFilterer.spec.ts +++ b/src/DiagnosticFilterer.spec.ts @@ -1,6 +1,7 @@ import { expect } from './chai-config.spec'; import { DiagnosticFilterer } from './DiagnosticFilterer'; import type { BsDiagnostic } from './interfaces'; +import { Program } from './Program'; import util, { standardizePath as s } from './util'; import { createSandbox } from 'sinon'; const sinon = createSandbox(); @@ -12,14 +13,15 @@ describe('DiagnosticFilterer', () => { let options = { rootDir: rootDir, diagnosticFilters: [ + 'codename', //ignore these codes globally { codes: [1, 2, 3, 'X4'] }, //ignore all codes from lib - { src: 'lib/**/*.brs' }, + { files: 'lib/**/*.brs' }, //ignore all codes from `packages` with absolute path - { src: `${rootDir}/packages/**/*.brs` }, + { files: `${rootDir}/packages/**/*.brs` }, //ignore specific codes for main.brs - { src: 'source/main.brs', codes: [4] } + { files: 'source/main.brs', codes: [4] } ] }; @@ -86,24 +88,34 @@ describe('DiagnosticFilterer', () => { ).to.eql([11, 12, 13, 'X14']); }); + + it('works with single file src glob', () => { + expect( + filterer.filter(options, [ + getDiagnostic('codename', `${rootDir}/source/main.brs`) //remove + ]).map(x => x.code) + ).to.eql([]); + }); + describe('with negative globs', () => { let optionsWithNegatives = { rootDir: rootDir, diagnosticFilters: [ //ignore these codes globally - { codes: [1, 2] }, + { codes: [1, 2, 'codename'] }, 3, 4, + 'codename', //ignore all codes from lib - { src: 'lib/**/*.brs' }, + { files: 'lib/**/*.brs' }, //un-ignore specific errors from lib/special - { src: '!lib/special/**/*.brs', codes: [1, 2, 3] }, + { files: '!lib/special/**/*.brs', codes: [1, 2, 3, 'codename'] }, //re-ignore errors from one extra special file - { src: 'lib/special/all-reignored.brs' }, + { files: 'lib/special/all-reignored.brs' }, //un-ignore all codes from third special file - { src: '!lib/special/all-unignored.brs' }, + { files: '!lib/special/all-unignored.brs' }, //un-ignore code 5 globally - { src: '!*/**/*', codes: [5] }, + { files: '!*/**/*', codes: [5] }, //re-ignore code 10 globally, overriding previous unignores { codes: [10] } ] @@ -180,7 +192,7 @@ describe('DiagnosticFilterer', () => { it('handles standard diagnostic filters', () => { expect( filterer.getDiagnosticFilters({ - diagnosticFilters: [{ src: 'file.brs', codes: [1, 2, 'X3'] }] + diagnosticFilters: [{ files: 'file.brs', codes: [1, 2, 'X3'] }] }) ).to.eql([{ src: 'file.brs', codes: [1, 2, 'X3'], isNegative: false }]); }); @@ -188,7 +200,7 @@ describe('DiagnosticFilterer', () => { it('handles string-only diagnostic filter object', () => { expect( filterer.getDiagnosticFilters({ - diagnosticFilters: [{ src: 'file.brs' }] + diagnosticFilters: [{ files: 'file.brs' }] }) ).to.eql([{ src: 'file.brs', isNegative: false }]); }); @@ -204,9 +216,9 @@ describe('DiagnosticFilterer', () => { it('handles string diagnostic filter', () => { expect( filterer.getDiagnosticFilters({ - diagnosticFilters: ['file.brs'] + diagnosticFilters: ['cannot-find-name'] }) - ).to.eql([{ src: 'file.brs', isNegative: false }]); + ).to.eql([{ codes: ['cannot-find-name'], isNegative: false }]); }); it('converts ignoreErrorCodes to diagnosticFilters', () => { @@ -217,19 +229,11 @@ describe('DiagnosticFilterer', () => { ]); }); - it('handles negative globs in bare strings', () => { - expect(filterer.getDiagnosticFilters({ - diagnosticFilters: ['!file.brs'] - })).to.eql([ - { src: 'file.brs', isNegative: true } - ]); - }); - it('handles negative globs in objects', () => { expect(filterer.getDiagnosticFilters({ diagnosticFilters: [ { - src: '!file.brs' + files: '!file.brs' } ] })).to.eql([ @@ -241,7 +245,7 @@ describe('DiagnosticFilterer', () => { expect(filterer.getDiagnosticFilters({ diagnosticFilters: [ { - src: '!file.brs', + files: '!file.brs', codes: [1, 2, 3] } ] @@ -251,6 +255,54 @@ describe('DiagnosticFilterer', () => { }); }); + describe('filtering by dest', () => { + it('will filter by a files destPath', () => { + const program = new Program({ rootDir: rootDir }); + program.setFile('source/common.brs', ''); + program.setFile({ src: `${rootDir}/source/utils.brs`, dest: `source/remove/utils.brs` }, ''); + program.setFile({ src: `${rootDir}/components/utils.brs`, dest: `source/remove/utils2.brs` }, ''); + + const resultDiagnostics = filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { + files: [ + { dest: 'source/remove/**/*.*' } + ], + codes: ['diagCode'] + } + ] + }, [ + getDiagnostic('diagCode', `${rootDir}/source/common.brs`), //keep + getDiagnostic('diagCode', `${rootDir}/source/utils.brs`, `${rootDir}/source/remove/utils.brs`), //remove + getDiagnostic('diagCode', `${rootDir}/components/utils.brs`, `${rootDir}/source/remove/utils2.brs`) //remove + ], program); + expect(resultDiagnostics.map(x => x.code)).to.eql(['diagCode']); + expect(resultDiagnostics.map(x => x.location.uri)).to.eql([util.pathToUri(s`${rootDir}/source/common.brs`)]); + }); + + it('respects order of ignores with negative globs', () => { + const resultDiagnostics = filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [{ + files: [ + { dest: 'source/**/*.*' } //ignore diagCode in files with destPath in /source/remove + ], + codes: ['diagCode'] + }, { + files: '!**/*.*', //unignore diagCode everywhere + codes: ['diagCode'] + } + ] + }, [ + getDiagnostic('diagCode', `${rootDir}/source/common.brs`), //keep + getDiagnostic('diagCode', `${rootDir}/source/utils.brs`, `${rootDir}/source/remove/utils.brs`), //remove + getDiagnostic('diagCode', `${rootDir}/components/utils.brs`, `${rootDir}/source/remove/utils2.brs`) //remove + ]); + expect(resultDiagnostics.map(x => x.code)).to.eql(['diagCode', 'diagCode', 'diagCode']); + }); + }); + it('only filters by file once per unique file (case-insensitive)', () => { const stub = sinon.stub(filterer as any, 'filterFile').returns(null); filterer.filter(options, [ @@ -259,8 +311,16 @@ describe('DiagnosticFilterer', () => { getDiagnostic(3, s`${rootDir}/source/common2.brs`), getDiagnostic(4, s`${rootDir}/source/Common2.brs`) ]); - expect(stub.callCount).to.eql(2); + const expectedCallCount = options.diagnosticFilters.reduce((acc, filter) => { + if (typeof filter === 'object' && 'files' in (filter as any)) { + return acc; + } + return acc + 1; + }, 0); + expect(stub.callCount).to.eql(expectedCallCount * 2); // 2 times for 'codename', 2 times for { codes: [1, 2, 3, 'X4'] } expect(stub.getCalls().map(x => x.args[1].toLowerCase())).to.eql([ + util.pathToUri(s`${rootDir.toLowerCase()}/source/common1.brs`).toLowerCase(), + util.pathToUri(s`${rootDir.toLowerCase()}/source/common2.brs`).toLowerCase(), util.pathToUri(s`${rootDir.toLowerCase()}/source/common1.brs`).toLowerCase(), util.pathToUri(s`${rootDir.toLowerCase()}/source/common2.brs`).toLowerCase() ]); @@ -268,7 +328,8 @@ describe('DiagnosticFilterer', () => { }); -function getDiagnostic(code: number | string, srcPath: string) { +function getDiagnostic(code: number | string, srcPath: string, destPath?: string) { + destPath = destPath ?? srcPath; return { location: { uri: util.pathToUri(s`${srcPath}`) diff --git a/src/DiagnosticFilterer.ts b/src/DiagnosticFilterer.ts index db84fce5c..91e4b6042 100644 --- a/src/DiagnosticFilterer.ts +++ b/src/DiagnosticFilterer.ts @@ -4,6 +4,7 @@ import * as minimatch from 'minimatch'; import type { BsConfig } from './BsConfig'; import util, { standardizePath as s } from './util'; import { URI } from 'vscode-uri'; +import type { Program } from './Program'; interface DiagnosticWithSuppression { diagnostic: BsDiagnostic; @@ -12,28 +13,31 @@ interface DiagnosticWithSuppression { interface NormalizedFilter { src?: string; + dest?: string; codes?: (number | string)[]; isNegative: boolean; } export class DiagnosticFilterer { private byFile: Record; + private fileDestSrcUriMap: Record; private filters: NormalizedFilter[] | undefined; private rootDir: string | undefined; constructor() { this.byFile = {}; + this.fileDestSrcUriMap = {}; } /** * Filter a list of diagnostics based on the provided filters */ - public filter(options: BsConfig, diagnostics: BsDiagnostic[]) { + public filter(options: BsConfig, diagnostics: BsDiagnostic[], program?: Program) { this.filters = this.getDiagnosticFilters(options); this.rootDir = options.rootDir; - this.groupByFile(diagnostics); + this.groupByFile(diagnostics, program); for (let filter of this.filters) { this.filterAllFiles(filter); @@ -42,6 +46,7 @@ export class DiagnosticFilterer { //clean up this.byFile = {}; + this.fileDestSrcUriMap = {}; delete this.rootDir; delete this.filters; @@ -70,9 +75,9 @@ export class DiagnosticFilterer { /** * group the diagnostics by file */ - private groupByFile(diagnostics: BsDiagnostic[]) { + private groupByFile(diagnostics: BsDiagnostic[], program?: Program) { this.byFile = {}; - + this.fileDestSrcUriMap = {}; for (let diagnostic of diagnostics) { const fileUri = diagnostic?.location?.uri ?? 'invalid-uri'; //skip diagnostics that have issues @@ -88,15 +93,24 @@ export class DiagnosticFilterer { diagnostic: diagnostic, isSuppressed: false }); + + if (program) { + const fileForDiagnostic = program.getFile(diagnostic.location?.uri); + if (fileForDiagnostic) { + const lowerDestPath = fileForDiagnostic.destPath.toLowerCase(); + this.fileDestSrcUriMap[lowerDestPath] = diagnostic.location?.uri; + } + } } } private filterAllFiles(filter: NormalizedFilter) { let matchedFileUris: string[]; - //if there's a src, match against all files if (filter.src) { - //prepend rootDir to src if the filter is a relative path + //if there's a src, match against all files + + //prepend rootDir to src if the filter is not a relative path let src = s( path.isAbsolute(filter.src) ? filter.src : `${this.rootDir}/${filter.src}` ); @@ -106,7 +120,17 @@ export class DiagnosticFilterer { nocase: true }).map(src => util.pathToUri(src).toLowerCase()); - //there is no src; this applies to all files + } else if (filter.dest) { + // applies to file dest location + + // search against the set of file destinations + const byFileDests = Object.keys(this.fileDestSrcUriMap); + matchedFileUris = minimatch.match(byFileDests, filter.dest, { + nocase: true + }).map((destPath) => { + return this.fileDestSrcUriMap[destPath]?.toLowerCase(); + }); + } else { matchedFileUris = Object.keys(this.byFile); } @@ -118,6 +142,9 @@ export class DiagnosticFilterer { } private filterFile(filter: NormalizedFilter, fileUri: string) { + if (!fileUri) { + return; + } //if the filter is negative, we're turning diagnostics on //if the filter is not negative we're turning diagnostics off const isSuppressing = !filter.isNegative; @@ -132,7 +159,10 @@ export class DiagnosticFilterer { } else { let fileDiagnostics = this.byFile[lowerFileUri]; for (const diagnostic of fileDiagnostics) { - if (filter.codes.includes(diagnostic.diagnostic.code!)) { + if (filter.codes.includes(diagnostic.diagnostic.code!) || + (diagnostic.diagnostic.legacyCode && + (filter.codes.includes(diagnostic.diagnostic.legacyCode) || + filter.codes.includes(diagnostic.diagnostic.legacyCode.toString())))) { diagnostic.isSuppressed = isSuppressing; } } @@ -155,7 +185,7 @@ export class DiagnosticFilterer { } for (let filter of diagnosticFilters) { - if (typeof filter === 'number') { + if (typeof filter === 'number' || typeof filter === 'string') { result.push({ codes: [filter], isNegative: false @@ -163,24 +193,13 @@ export class DiagnosticFilterer { continue; } - if (typeof filter === 'string') { - const isNegative = filter.startsWith('!'); - const trimmedFilter = isNegative ? filter.slice(1) : filter; - - result.push({ - src: trimmedFilter, - isNegative: isNegative - }); - continue; - } - //filter out bad inputs if (!filter || typeof filter !== 'object') { continue; } //code-only filter - if ('codes' in filter && !('src' in filter) && Array.isArray(filter.codes)) { + if ('codes' in filter && !('files' in filter) && Array.isArray(filter.codes)) { result.push({ codes: filter.codes, isNegative: false @@ -188,24 +207,67 @@ export class DiagnosticFilterer { continue; } - if ('src' in filter) { - const isNegative = filter.src.startsWith('!'); - const trimmedFilter = isNegative ? filter.src.slice(1) : filter.src; - - if ('codes' in filter) { - result.push({ - src: trimmedFilter, - codes: filter.codes, - isNegative: isNegative - }); - } else { - result.push({ - src: trimmedFilter, - isNegative: isNegative - }); + if ('files' in filter) { + if (typeof filter.files === 'string') { + result.push(this.getNormalizedFilter(filter.files, filter)); + continue; + } + + if (Array.isArray(filter.files)) { + for (const fileIdentifier of filter.files) { + if (typeof fileIdentifier === 'string') { + result.push(this.getNormalizedFilter(fileIdentifier, filter)); + continue; + } + if (typeof fileIdentifier === 'object') { + if ('src' in fileIdentifier) { + result.push(this.getNormalizedFilter(fileIdentifier.src, filter)); + continue; + } + if ('dest' in fileIdentifier) { + result.push(this.getNormalizedFilter(fileIdentifier.dest, filter, 'dest')); + continue; + } + } + } } } } return result; } + + + private getNormalizedFilter(fileGlob: string, filter: { files: string } | { codes?: (number | string)[] }, locationKey: 'src' | 'dest' = 'src'): NormalizedFilter { + const isNegative = fileGlob.startsWith('!'); + const trimmedFilter = isNegative ? fileGlob.slice(1) : fileGlob; + if (locationKey === 'src') { + if ('codes' in filter && Array.isArray(filter.codes)) { + return { + src: trimmedFilter, + codes: filter.codes, + isNegative: isNegative + }; + } else { + return { + src: trimmedFilter, + isNegative: isNegative + }; + } + } else { + // dest + if ('codes' in filter && Array.isArray(filter.codes)) { + return { + dest: trimmedFilter, + codes: filter.codes, + isNegative: isNegative + }; + } else { + return { + dest: trimmedFilter, + isNegative: isNegative + }; + } + } + + } } diff --git a/src/DiagnosticManager.ts b/src/DiagnosticManager.ts index 6607aca32..140788b27 100644 --- a/src/DiagnosticManager.ts +++ b/src/DiagnosticManager.ts @@ -135,14 +135,17 @@ export class DiagnosticManager { * Determine whether this diagnostic should be supressed or not, based on brs comment-flags */ public isDiagnosticSuppressed(diagnostic: BsDiagnostic) { - const diagnosticCode = typeof diagnostic.code === 'string' ? diagnostic.code.toLowerCase() : diagnostic.code; + const diagnosticCode = typeof diagnostic.code === 'string' ? diagnostic.code.toLowerCase() : diagnostic.code?.toString() ?? undefined; + const diagnosticLegacyCode = typeof diagnostic.legacyCode === 'string' ? diagnostic.legacyCode.toLowerCase() : diagnostic.legacyCode; const file = this.program?.getFile(diagnostic.location?.uri); for (let flag of file?.commentFlags ?? []) { //this diagnostic is affected by this flag if (diagnostic.location.range && util.rangeContains(flag.affectedRange, diagnostic.location.range.start)) { //if the flag acts upon this diagnostic's code - if (flag.codes === null || (diagnosticCode !== undefined && flag.codes.includes(diagnosticCode))) { + const diagCodeSuppressed = (diagnosticCode !== undefined && flag.codes?.includes(diagnosticCode)) || + (diagnosticLegacyCode !== undefined && flag.codes?.includes(diagnosticLegacyCode)); + if (flag.codes === null || diagCodeSuppressed) { return true; } } @@ -155,7 +158,7 @@ export class DiagnosticManager { let filteredDiagnostics = this.diagnosticFilterer.filter({ ...this.options ?? {}, rootDir: this.options?.rootDir - }, diagnostics); + }, diagnostics, this.program); return filteredDiagnostics; } diff --git a/src/DiagnosticMessages.spec.ts b/src/DiagnosticMessages.spec.ts index fa54bfc95..a34506fec 100644 --- a/src/DiagnosticMessages.spec.ts +++ b/src/DiagnosticMessages.spec.ts @@ -1,19 +1,56 @@ import { expect } from './chai-config.spec'; -import { DiagnosticMessages } from './DiagnosticMessages'; +import { DiagnosticCodeRegex, DiagnosticMessages } from './DiagnosticMessages'; describe('DiagnosticMessages', () => { + it('has unique legacyCode for each message', () => { + let codes = {}; + for (let key in DiagnosticMessages) { + let func = DiagnosticMessages[key]; + let obj = func('', '', '', '', '', '', '', '', ''); + if (obj.legacyCode === undefined) { + // does not have a legacy code + continue; + } + + //if another message already has this code + if (!codes[obj.legacyCode]) { + codes[obj.legacyCode] = key; + } else { + expect(codes[obj.legacyCode]).to.equal(key, 'Two diagnostic messages share the same legacy Code'); + } + } + }); + it('has unique code for each message', () => { let codes = {}; for (let key in DiagnosticMessages) { + if (key.startsWith('__unused')) { + // ignore unused diagnostics + continue; + } let func = DiagnosticMessages[key]; let obj = func('', '', '', '', '', '', '', '', ''); + const diagCode: string = obj.code ?? ''; + expect(diagCode).to.not.equal('', `Diagnostic name is empty - ${key}`); + expect(diagCode.toLowerCase()).to.equal(obj.code, `Diagnostic name has capitals - ${key}`); + expect(diagCode.indexOf(' ')).to.equal(-1, `Diagnostic name has space - ${key}`); + expect(DiagnosticCodeRegex.test(diagCode)).to.equal(true, `Diagnostic name does not match regex - ${key}`); //if another message already has this code if (!codes[obj.code]) { codes[obj.code] = key; } else { - expect(codes[obj.code]).to.equal(key, 'Two diagnostic messages share the same error code'); + expect(codes[obj.code]).to.equal(key, 'Two diagnostic messages share the same error codes'); } } }); + + it('properly formats expected terminator diagnostics', () => { + let diag = DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'); + expect(diag.message).to.equal(`Expected 'end if', 'else if' or 'else' to terminate 'then' block`); + diag = DiagnosticMessages.expectedTerminator('end try', 'try-catch'); + expect(diag.message).to.equal(`Expected 'end try' to terminate 'try-catch' statement`); + diag = DiagnosticMessages.expectedTerminator(['one', 'two'], 'something'); + expect(diag.message).to.equal(`Expected 'one' or 'two' to terminate 'something' statement`); + }); }); diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index a02d2ac50..d7d8248ee 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -5,6 +5,9 @@ import { TokenKind } from './lexer/TokenKind'; import util from './util'; import { SymbolTypeFlag } from './SymbolTypeFlag'; + +export const DiagnosticCodeRegex = /^[a-z](?:[a-z0-9]*(?:-[a-z0-9]+)*)*$/; + /** * An object that keeps track of all possible error messages. */ @@ -12,8 +15,9 @@ export let DiagnosticMessages = { //this one won't be used much, we just need a catchall object for the code since we pass through the message from the parser genericParserMessage: (message: string) => ({ message: message, - code: 1000, - severity: DiagnosticSeverity.Error + legacyCode: 1000, + severity: DiagnosticSeverity.Error, + code: 'generic-parser-message' }), /** * @@ -24,711 +28,907 @@ export let DiagnosticMessages = { */ cannotFindName: (name: string, fullName?: string, typeName?: string, typeDescriptor = 'type') => ({ message: `Cannot find name '${name}'${typeName ? ` for ${typeDescriptor} '${typeName}'` : ''}`, - code: 1001, + legacyCode: 1001, data: { name: name, fullName: fullName ?? name, typeName: typeName ? typeName : undefined }, - severity: DiagnosticSeverity.Error + severity: DiagnosticSeverity.Error, + code: 'cannot-find-name' }), mismatchArgumentCount: (expectedCount: number | string, actualCount: number) => ({ message: `Expected ${expectedCount} arguments, but got ${actualCount}.`, - code: 1002, - severity: DiagnosticSeverity.Error + legacyCode: 1002, + severity: DiagnosticSeverity.Error, + code: 'incorrect-argument-count' }), duplicateFunctionImplementation: (functionName: string) => ({ message: `Duplicate function implementation for '${functionName}'.`, - code: 1003, - severity: DiagnosticSeverity.Error + legacyCode: 1003, + severity: DiagnosticSeverity.Error, + code: 'duplicate-function' }), referencedFileDoesNotExist: () => ({ message: `Referenced file does not exist.`, - code: 1004, - severity: DiagnosticSeverity.Error + legacyCode: 1004, + severity: DiagnosticSeverity.Error, + code: 'file-not-found' }), xmlComponentMissingComponentDeclaration: () => ({ message: `Missing a component declaration.`, - code: 1005, - severity: DiagnosticSeverity.Error + legacyCode: 1005, + severity: DiagnosticSeverity.Error, + code: 'missing-component-element' }), xmlComponentMissingNameAttribute: () => ({ message: `Component must have a name attribute.`, - code: 1006, - severity: DiagnosticSeverity.Error + legacyCode: 1006, + severity: DiagnosticSeverity.Error, + code: 'missing-name-attribute' }), xmlComponentMissingExtendsAttribute: () => ({ message: `Component is mising "extends" attribute and will automatically extend "Group" by default`, - code: 1007, - severity: DiagnosticSeverity.Warning + legacyCode: 1007, + severity: DiagnosticSeverity.Warning, + code: 'missing-extends-attribute' }), - xmlGenericParseError: (message: string) => ({ + syntaxError: (message: string) => ({ //generic catchall xml parse error message: message, - code: 1008, - severity: DiagnosticSeverity.Error + legacyCode: 1008, + severity: DiagnosticSeverity.Error, + code: 'syntax-error' }), unnecessaryScriptImportInChildFromParent: (parentComponentName: string) => ({ message: `Unnecessary script import: Script is already imported in ancestor component '${parentComponentName}'.`, - code: 1009, - severity: DiagnosticSeverity.Warning + legacyCode: 1009, + severity: DiagnosticSeverity.Warning, + code: 'redundant-import' }), overridesAncestorFunction: (callableName: string, currentScopeName: string, parentFilePath: string, parentScopeName: string) => ({ message: `Function '${callableName}' included in '${currentScopeName}' overrides function in '${parentFilePath}' included in '${parentScopeName}'.`, - code: 1010, - severity: DiagnosticSeverity.Hint + legacyCode: 1010, + severity: DiagnosticSeverity.Hint, + code: 'overrides-ancestor-function' }), localVarFunctionShadowsParentFunction: (scopeName: 'stdlib' | 'scope') => ({ message: `Local variable function has same name as ${scopeName} function and will never be called.`, - code: 1011, - severity: DiagnosticSeverity.Warning + legacyCode: 1011, + severity: DiagnosticSeverity.Warning, + code: 'variable-shadows-function' }), scriptImportCaseMismatch: (correctFilePath: string) => ({ message: `Script import path does not match casing of actual file path '${correctFilePath}'.`, - code: 1012, - severity: DiagnosticSeverity.Warning + legacyCode: 1012, + severity: DiagnosticSeverity.Warning, + code: 'import-case-mismatch' }), fileNotReferencedByAnyOtherFile: () => ({ message: `This file is not referenced by any other file in the project.`, - code: 1013, - severity: DiagnosticSeverity.Warning + legacyCode: 1013, + severity: DiagnosticSeverity.Warning, + code: 'file-not-referenced' }), - unknownDiagnosticCode: (theUnknownCode: number) => ({ + unknownDiagnosticCode: (theUnknownCode: number | string) => ({ message: `Unknown diagnostic code ${theUnknownCode}`, - code: 1014, - severity: DiagnosticSeverity.Warning + legacyCode: 1014, + severity: DiagnosticSeverity.Warning, + code: 'unknown-diagnostic-code' }), scriptSrcCannotBeEmpty: () => ({ message: `Script import cannot be empty or whitespace`, - code: 1015, - severity: DiagnosticSeverity.Error - }), - expectedIdentifierAfterKeyword: (keywordText: string) => ({ - message: `Expected identifier after '${keywordText}' keyword`, - code: 1016, - severity: DiagnosticSeverity.Error + legacyCode: 1015, + severity: DiagnosticSeverity.Error, + code: 'invalid-import-path' }), + expectedIdentifier: (preceedingTokenText?: string) => { + let message = `Expected identifier`; + if (preceedingTokenText) { + message += ` after '${preceedingTokenText}'`; + } + return { + message: message, + legacyCode: 1016, + severity: DiagnosticSeverity.Error, + code: 'expected-identifier' + }; + }, missingCallableKeyword: () => ({ - message: `Expected 'function' or 'sub' to preceed identifier`, - code: 1017, - severity: DiagnosticSeverity.Error + message: `Expected 'function' or 'sub' to precede identifier`, + legacyCode: 1017, + severity: DiagnosticSeverity.Error, + code: 'expected-leading-keyword' }), - expectedValidTypeToFollowAsKeyword: () => ({ + __unused12: () => ({ message: `Expected valid type to follow 'as' keyword`, - code: 1018, - severity: DiagnosticSeverity.Error + legacyCode: 1018, + severity: DiagnosticSeverity.Error, + code: 'expected-valid-type' }), bsFeatureNotSupportedInBrsFiles: (featureName) => ({ message: `BrighterScript feature '${featureName}' is not supported in standard BrightScript files`, - code: 1019, - severity: DiagnosticSeverity.Error + legacyCode: 1019, + severity: DiagnosticSeverity.Error, + code: 'bs-feature-not-supported' }), - brsConfigJsonIsDeprecated: () => ({ + __ununsed12: () => ({ message: `'brsconfig.json' is deprecated. Please rename to 'bsconfig.json'`, - code: 1020, - severity: DiagnosticSeverity.Warning + legacyCode: 1020, + severity: DiagnosticSeverity.Warning, + code: 'brsconfig-deprecated' }), bsConfigJsonHasSyntaxErrors: (message: string) => ({ message: `Encountered syntax errors in bsconfig.json: ${message}`, - code: 1021, - severity: DiagnosticSeverity.Error - }), - itemIsDeprecated: () => ({ - message: `Item is deprecated`, - code: 1022, - severity: DiagnosticSeverity.Hint + legacyCode: 1021, + severity: DiagnosticSeverity.Error, + code: 'bsconfig-syntax-errors' }), + itemIsDeprecated: (itemName?: string, deprecatedDescription?: string) => { + itemName ??= 'Item'; + return { + message: `${itemName} has been deprecated${deprecatedDescription ? ': ' + deprecatedDescription : ''} `, + legacyCode: 1022, + severity: DiagnosticSeverity.Hint, + code: 'item-deprecated' + }; + }, cannotUseOverrideKeywordOnConstructorFunction: () => ({ message: 'Override keyword is not allowed on class constructor method', - code: 1023, - severity: DiagnosticSeverity.Error + legacyCode: 1023, + severity: DiagnosticSeverity.Error, + code: 'override-keyword-on-constructor' }), - statementMustBeDeclaredAtTopOfFile: (statementKeyword: string) => ({ - message: `'${statementKeyword}' statement must be declared at the top of the file`, - code: 1024, - severity: DiagnosticSeverity.Error + unexpectedStatementLocation: (statementKeyword: string, locationText: string) => ({ + message: `'${statementKeyword}' statement must be declared ${locationText}`, + legacyCode: 1024, + severity: DiagnosticSeverity.Error, + code: 'unexpected-statement-location' }), - methodDoesNotExistOnType: (methodName: string, className: string) => ({ + __unused8: (methodName: string, className: string) => ({ message: `Method '${methodName}' does not exist on type '${className}'`, - code: 1025, + legacyCode: 1025, severity: DiagnosticSeverity.Error }), duplicateIdentifier: (memberName: string) => ({ message: `Duplicate identifier '${memberName}'`, - code: 1026, - severity: DiagnosticSeverity.Error + legacyCode: 1026, + severity: DiagnosticSeverity.Error, + code: 'duplicate-identifier' }), missingOverrideKeyword: (ancestorClassName: string) => ({ message: `Method has no override keyword but is declared in ancestor class '${ancestorClassName}'`, - code: 1027, - severity: DiagnosticSeverity.Error + legacyCode: 1027, + severity: DiagnosticSeverity.Error, + code: 'missing-override-keyword' }), nameCollision: (thisThingKind: string, thatThingKind: string, thatThingName: string) => ({ message: `${thisThingKind} has same name as ${thatThingKind} '${thatThingName}'`, - code: 1028, - severity: DiagnosticSeverity.Error + legacyCode: 1028, + severity: DiagnosticSeverity.Error, + code: 'name-collision' }), - classCouldNotBeFound: (className: string, scopeName: string) => ({ + __unused9: (className: string, scopeName: string) => ({ message: `Class '${className}' could not be found when this file is included in scope '${scopeName}'`, - code: 1029, + legacyCode: 1029, severity: DiagnosticSeverity.Error, data: { className: className } }), - expectedClassFieldIdentifier: () => ({ + __unused27: () => ({ message: `Expected identifier in class body`, - code: 1030, - severity: DiagnosticSeverity.Error + legacyCode: 1030, + severity: DiagnosticSeverity.Error, + code: 'expected-identifier-in-body' }), expressionIsNotConstructable: (expressionType: string) => ({ message: `Cannot use the 'new' keyword here because '${expressionType}' is not a constructable type`, - code: 1031, - severity: DiagnosticSeverity.Error + legacyCode: 1031, + severity: DiagnosticSeverity.Error, + code: 'not-constructable' }), expectedKeyword: (kind: TokenKind) => ({ message: `Expected '${kind}' keyword`, - code: 1032, - severity: DiagnosticSeverity.Error + legacyCode: 1032, + severity: DiagnosticSeverity.Error, + code: 'expected-keyword' }), - expectedLeftParenAfterCallable: (callableType: string) => ({ + __unused28: (callableType: string) => ({ message: `Expected '(' after ${callableType}`, - code: 1033, - severity: DiagnosticSeverity.Error + legacyCode: 1033, + severity: DiagnosticSeverity.Error, + code: 'expected-left-paren-after-callable' }), - expectedNameAfterCallableKeyword: (callableType: string) => ({ + __unused29: (callableType: string) => ({ message: `Expected ${callableType} name after '${callableType}' keyword`, - code: 1034, - severity: DiagnosticSeverity.Error + legacyCode: 1034, + severity: DiagnosticSeverity.Error, + code: 'expected-name-after-callable' }), - expectedLeftParenAfterCallableName: (callableType: string) => ({ + __unused30: (callableType: string) => ({ message: `Expected '(' after ${callableType} name`, - code: 1035, - severity: DiagnosticSeverity.Error + legacyCode: 1035, + severity: DiagnosticSeverity.Error, + code: 'expected-left-paren-after-callable-name' }), tooManyCallableParameters: (actual: number, max: number) => ({ message: `Cannot have more than ${max} parameters but found ${actual})`, - code: 1036, - severity: DiagnosticSeverity.Error + legacyCode: 1036, + severity: DiagnosticSeverity.Error, + code: 'exceeds-max-parameter-count' }), __unused: (typeText: string) => ({ message: `Function return type '${typeText}' is invalid`, - code: 1037, + legacyCode: 1037, severity: DiagnosticSeverity.Error }), requiredParameterMayNotFollowOptionalParameter: (parameterName: string) => ({ message: `Required parameter '${parameterName}' must be declared before any optional parameters`, - code: 1038, - severity: DiagnosticSeverity.Error + legacyCode: 1038, + severity: DiagnosticSeverity.Error, + code: 'required-parameter-before-optional' }), expectedNewlineOrColon: () => ({ message: `Expected newline or ':' at the end of a statement`, - code: 1039, - severity: DiagnosticSeverity.Error + legacyCode: 1039, + severity: DiagnosticSeverity.Error, + code: 'expected-statement-separator' }), - functionNameCannotEndWithTypeDesignator: (callableType: string, name: string, designator: string) => ({ - message: `${callableType} name '${name}' cannot end with type designator '${designator}'`, - code: 1040, - severity: DiagnosticSeverity.Error + invalidIdentifier: (name: string, character: string) => ({ + message: `Identifier '${name}' may not contain the character '${character}'`, + legacyCode: 1040, + severity: DiagnosticSeverity.Error, + code: 'invalid-identifier' }), - callableBlockMissingEndKeyword: (callableType: string) => ({ + __unused31: (callableType: string) => ({ message: `Expected 'end ${callableType}' to terminate ${callableType} block`, - code: 1041, - severity: DiagnosticSeverity.Error - }), - mismatchedEndCallableKeyword: (expectedCallableType: string, actualCallableType: string) => ({ - message: `Expected 'end ${expectedCallableType?.replace(/^end\s*/, '')}' to terminate ${expectedCallableType} block but found 'end ${actualCallableType?.replace(/^end\s*/, '')}' instead.`, - code: 1042, - severity: DiagnosticSeverity.Error + legacyCode: 1041, + severity: DiagnosticSeverity.Error, + code: 'closing-keyword-mismatch' }), + closingKeywordMismatch: (expectedCallableType: string, actualCallableType: string) => { + let message = `Expected 'end ${expectedCallableType?.replace(/^end\s*/, '')}' to terminate ${expectedCallableType} block`; + if (actualCallableType) { + message += ` but found 'end ${actualCallableType?.replace(/^end\s*/, '')}' instead.`; + } + return { + message: message, + legacyCode: 1042, + severity: DiagnosticSeverity.Error, + code: 'closing-keyword' + }; + }, expectedParameterNameButFound: (text: string) => ({ message: `Expected parameter name, but found '${text ?? ''}'`, - code: 1043, - severity: DiagnosticSeverity.Error + legacyCode: 1043, + severity: DiagnosticSeverity.Error, + code: 'expected-parameter-name' }), __unused2: (parameterName: string, typeText: string) => ({ message: `Function parameter '${parameterName}' is of invalid type '${typeText}'`, - code: 1044, + legacyCode: 1044, severity: DiagnosticSeverity.Error }), cannotUseReservedWordAsIdentifier: (name: string) => ({ message: `Cannot use reserved word '${name}' as an identifier`, - code: 1045, - severity: DiagnosticSeverity.Error + legacyCode: 1045, + severity: DiagnosticSeverity.Error, + code: 'cannot-use-reserved-word' }), - expectedOperatorAfterIdentifier: (operators: TokenKind[], name: string) => { + expectedOperator: (operators: TokenKind[], name: string) => { operators = Array.isArray(operators) ? operators : []; + let message = `Expected operator ('${operators.join(`', '`)}')`; + if (name) { + message += ` after idenfifier '${name}'`; + } return { - message: `Expected operator ('${operators.join(`', '`)}') after idenfifier '${name}'`, - code: 1046, - severity: DiagnosticSeverity.Error + message: message, + legacyCode: 1046, + severity: DiagnosticSeverity.Error, + code: 'expected-operator' }; }, expectedInlineIfStatement: () => ({ message: `If/else statement within an inline if should be also inline`, - code: 1047, - severity: DiagnosticSeverity.Error + legacyCode: 1047, + severity: DiagnosticSeverity.Error, + code: 'malformed-inline-if' }), expectedFinalNewline: () => ({ message: `Expected newline at the end of an inline if statement`, - code: 1048, - severity: DiagnosticSeverity.Error + legacyCode: 1048, + severity: DiagnosticSeverity.Error, + code: 'expected-final-newline' }), couldNotFindMatchingEndKeyword: (keyword: string) => ({ message: `Could not find matching 'end ${keyword}'`, - code: 1049, - severity: DiagnosticSeverity.Error + legacyCode: 1049, + severity: DiagnosticSeverity.Error, + code: 'expected-end-keyword' }), expectedCatchBlockInTryCatch: () => ({ message: `Expected 'catch' block in 'try' statement`, - code: 1050, - severity: DiagnosticSeverity.Error + legacyCode: 1050, + severity: DiagnosticSeverity.Error, + code: 'expected-catch' }), - expectedEndForOrNextToTerminateForLoop: () => ({ - message: `Expected 'end for' or 'next' to terminate 'for' loop`, - code: 1051, - severity: DiagnosticSeverity.Error + expectedEndForOrNextToTerminateForLoop: (forLoopNameText: string = TokenKind.For) => ({ + message: `Expected 'end for' or 'next' to terminate '${forLoopNameText}' loop`, + legacyCode: 1051, + severity: DiagnosticSeverity.Error, + code: 'expected-loop-terminator' }), - expectedInAfterForEach: (name: string) => ({ + __unused32: (name: string) => ({ message: `Expected 'in' after 'for each ${name}'`, - code: 1052, - severity: DiagnosticSeverity.Error + legacyCode: 1052, + severity: DiagnosticSeverity.Error, + code: 'expected-in-for-each' }), expectedExpressionAfterForEachIn: () => ({ message: `Expected expression after 'in' keyword from 'for each' statement`, - code: 1053, - severity: DiagnosticSeverity.Error + legacyCode: 1053, + severity: DiagnosticSeverity.Error, + code: 'expected-loop-expression' }), unexpectedColonBeforeIfStatement: () => ({ message: `Colon before 'if' statement is not allowed`, - code: 1054, - severity: DiagnosticSeverity.Error + legacyCode: 1054, + severity: DiagnosticSeverity.Error, + code: 'unexpected-leading-colon' }), expectedStringLiteralAfterKeyword: (keyword: string) => ({ - message: `Missing string literal after '${keyword}' keyword`, - code: 1055, - severity: DiagnosticSeverity.Error + message: `Expected string literal after '${keyword}' keyword`, + legacyCode: 1055, + severity: DiagnosticSeverity.Error, + code: 'expected-string-literal' }), keywordMustBeDeclaredAtRootLevel: (keyword: string) => ({ message: `${keyword} must be declared at the root level`, - code: 1056, - severity: DiagnosticSeverity.Error + legacyCode: 1056, + severity: DiagnosticSeverity.Error, + code: 'keyword-must-be-root-level' }), __unused5: () => ({ message: `'library' statement must be declared at the top of the file`, - code: 1057, + legacyCode: 1057, severity: DiagnosticSeverity.Error }), - expectedEndIfElseIfOrElseToTerminateThenBlock: () => ({ + expectedTerminator: (expectedTerminators: string[] | string, statementType: string, blockDescriptor: 'block' | 'statement' = 'statement') => ({ + message: `Expected ${getPossibilitiesString(expectedTerminators)} to terminate '${statementType}' ${blockDescriptor}`, + severity: DiagnosticSeverity.Error, + code: 'expected-terminator' + }), + __unused14: () => ({ message: `Expected 'end if', 'else if', or 'else' to terminate 'then' block`, - code: 1058, - severity: DiagnosticSeverity.Error + legacyCode: 1058, + severity: DiagnosticSeverity.Error, + code: 'expected-terminator-on-then' }), - expectedEndTryToTerminateTryCatch: () => ({ + __unused15: () => ({ message: `Expected 'end try' to terminate 'try-catch' statement`, - code: 1059, - severity: DiagnosticSeverity.Error + legacyCode: 1059, + severity: DiagnosticSeverity.Error, + code: 'expected-terminator-on-try-catch' }), - expectedEndIfToCloseIfStatement: (startingPosition: Position) => ({ - message: `Expected 'end if' to close 'if' statement started at ${startingPosition?.line + 1}:${startingPosition?.character + 1}`, - code: 1060, - severity: DiagnosticSeverity.Error + __unused16: (startingPosition: Position) => ({ + message: `Expected 'end if' to close 'if' statement started at ${startingPosition?.line + 1}:${startingPosition?.character + 1} `, + legacyCode: 1060, + severity: DiagnosticSeverity.Error, + code: 'expected-terminator-on-if' }), - expectedStatementToFollowConditionalCondition: (conditionType: string) => ({ + expectedStatement: (conditionType?: string, extraDetail?: string) => { + let message = 'Expected statement'; + if (conditionType) { + message += ` to follow '${conditionType?.toLowerCase()}'`; + } + if (extraDetail) { + message += ` ${extraDetail}`; + } + return { + message: message, + severity: DiagnosticSeverity.Error, + code: 'expected-statement' + }; + }, + __unused18: (conditionType: string) => ({ message: `Expected a statement to follow '${conditionType?.toLowerCase()} ...condition... then'`, - code: 1061, - severity: DiagnosticSeverity.Error + legacyCode: 1061, + severity: DiagnosticSeverity.Error, + code: 'expected-statement-after-conditional' }), - expectedStatementToFollowElse: () => ({ + __unused19: () => ({ message: `Expected a statement to follow 'else'`, - code: 1062, - severity: DiagnosticSeverity.Error + legacyCode: 1062, + severity: DiagnosticSeverity.Error, + code: 'expected-statement-after-else' }), - consecutiveIncrementDecrementOperatorsAreNotAllowed: () => ({ - message: `Consecutive increment/decrement operators are not allowed`, - code: 1063, - severity: DiagnosticSeverity.Error + unexpectedOperator: () => ({ + message: `Unexpected operator`, + legacyCode: 1063, + severity: DiagnosticSeverity.Error, + code: 'unexpected-operator' }), - incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall: () => ({ - message: ``, - code: 1064, - severity: DiagnosticSeverity.Error + __unused13: () => ({ + message: `Increment / decrement operators are not allowed on function calls`, + legacyCode: 1064, + severity: DiagnosticSeverity.Error, + code: 'increment-decrement-on-function-call' }), xmlUnexpectedTag: (tagName: string) => ({ message: `Unexpected tag '${tagName}'`, - code: 1065, - severity: DiagnosticSeverity.Error + legacyCode: 1065, + severity: DiagnosticSeverity.Error, + code: 'unexpected-tag' }), - expectedStatementOrFunctionCallButReceivedExpression: () => ({ + __unused20: () => ({ message: `Expected statement or function call but instead found expression`, - code: 1066, - severity: DiagnosticSeverity.Error + legacyCode: 1066, + severity: DiagnosticSeverity.Error, + code: 'expected-statement-not-expression' }), xmlFunctionNotFound: (name: string) => ({ message: `Cannot find function with name '${name}' in component scope`, - code: 1067, - severity: DiagnosticSeverity.Error + legacyCode: 1067, + severity: DiagnosticSeverity.Error, + code: 'function-not-found' }), xmlInvalidFieldType: (name: string) => ({ message: `Invalid field type ${name}`, - code: 1068, - severity: DiagnosticSeverity.Error + legacyCode: 1068, + severity: DiagnosticSeverity.Error, + code: 'invalid-field-type' }), xmlUnexpectedChildren: (tagName: string) => ({ message: `Tag '${tagName}' should not have children`, - code: 1069, - severity: DiagnosticSeverity.Error + legacyCode: 1069, + severity: DiagnosticSeverity.Error, + code: 'unexpected-children' }), xmlTagMissingAttribute: (tagName: string, attrName: string) => ({ message: `Tag '${tagName}' must have a '${attrName}' attribute`, - code: 1070, - severity: DiagnosticSeverity.Error + legacyCode: 1070, + severity: DiagnosticSeverity.Error, + code: 'expected-attribute' }), expectedLabelIdentifierAfterGotoKeyword: () => ({ message: `Expected label identifier after 'goto' keyword`, - code: 1071, - severity: DiagnosticSeverity.Error + legacyCode: 1071, + severity: DiagnosticSeverity.Error, + code: 'expected-label' }), - expectedRightSquareBraceAfterArrayOrObjectIndex: () => ({ + __unused26: () => ({ message: `Expected ']' after array or object index`, - code: 1072, - severity: DiagnosticSeverity.Error + legacyCode: 1072, + severity: DiagnosticSeverity.Error, + code: 'expected-right-brace' }), - expectedPropertyNameAfterPeriod: () => ({ + __unused21: () => ({ message: `Expected property name after '.'`, - code: 1073, - severity: DiagnosticSeverity.Error + legacyCode: 1073, + severity: DiagnosticSeverity.Error, + code: 'expected-property-name' }), tooManyCallableArguments: (actual: number, max: number) => ({ - message: `Cannot have more than ${max} arguments but found ${actual}`, - code: 1074, - severity: DiagnosticSeverity.Error - }), - expectedRightParenAfterFunctionCallArguments: () => ({ - message: `Expected ')' after function call arguments`, - code: 1075, - severity: DiagnosticSeverity.Error + message: `Cannot have more than ${max} arguments but found ${actual} `, + legacyCode: 1074, + severity: DiagnosticSeverity.Error, + code: 'exceeds-max-argument-count' }), - unmatchedLeftParenAfterExpression: () => ({ + /** + * @param unmatchedToken Should be one of '(', '[', or '{' + * @param afterDetail any additional message to describe what came before the unmatched token + */ + unmatchedLeftToken: (unmatchedToken: string, afterDetail = '') => { + let matchingToken = ''; + switch (unmatchedToken) { + case '(': + matchingToken = ')'; + break; + case '[': + matchingToken = ']'; + break; + case '{': + matchingToken = '}'; + break; + } + let message = `Unmatched '${unmatchedToken}'`; + if (matchingToken) { + message += `: expected '${matchingToken}'`; + } + if (afterDetail) { + message += ` after ${afterDetail}`; + } + return { + message: message, + legacyCode: 1075, + severity: DiagnosticSeverity.Error, + code: 'unmatched-left-token' + }; + }, + __unused23: () => ({ message: `Unmatched '(': expected ')' after expression`, - code: 1076, - severity: DiagnosticSeverity.Error + legacyCode: 1076, + severity: DiagnosticSeverity.Error, + code: 'unmatched-left-paren' }), - unmatchedLeftSquareBraceAfterArrayLiteral: () => ({ + __unused24: () => ({ message: `Unmatched '[': expected ']' after array literal`, - code: 1077, - severity: DiagnosticSeverity.Error + legacyCode: 1077, + severity: DiagnosticSeverity.Error, + code: 'unmatched-left-brace' }), unexpectedAAKey: () => ({ message: `Expected identifier or string as associative array key`, - code: 1078, - severity: DiagnosticSeverity.Error + legacyCode: 1078, + severity: DiagnosticSeverity.Error, + code: 'invalid-aa-key' }), expectedColonBetweenAAKeyAndvalue: () => ({ message: `Expected ':' between associative array key and value`, - code: 1079, - severity: DiagnosticSeverity.Error + legacyCode: 1079, + severity: DiagnosticSeverity.Error, + code: 'expected-aa-separator' }), - unmatchedLeftCurlyAfterAALiteral: () => ({ + __unused25: () => ({ message: `Unmatched '{': expected '}' after associative array literal`, - code: 1080, - severity: DiagnosticSeverity.Error + legacyCode: 1080, + severity: DiagnosticSeverity.Error, + code: 'unmatched-left-curly' }), unexpectedToken: (text: string) => ({ message: `Unexpected token '${text}'`, - code: 1081, - severity: DiagnosticSeverity.Error + legacyCode: 1081, + severity: DiagnosticSeverity.Error, + code: 'unexpected-token' }), /** * Used in the lexer anytime we encounter an unsupported character */ unexpectedCharacter: (text: string) => ({ - message: `Unexpected character '${text}' (char code ${text?.charCodeAt(0)})`, - code: 1082, - severity: DiagnosticSeverity.Error + message: `Unexpected character '${text}'(char code ${text?.charCodeAt(0)})`, + legacyCode: 1082, + severity: DiagnosticSeverity.Error, + code: 'unexpected-character' }), - unterminatedStringAtEndOfLine: () => ({ - message: `Unterminated string at end of line`, - code: 1083, - severity: DiagnosticSeverity.Error + unterminatedString: () => ({ + message: `Unterminated string literal`, + legacyCode: 1083, + severity: DiagnosticSeverity.Error, + code: 'unterminated-string' }), - unterminatedStringAtEndOfFile: () => ({ + __unused33: () => ({ message: `Unterminated string at end of file`, - code: 1084, - severity: DiagnosticSeverity.Error + legacyCode: 1084, + severity: DiagnosticSeverity.Error, + code: 'unterminated-string-at-end-of-file' }), fractionalHexLiteralsAreNotSupported: () => ({ message: `Fractional hex literals are not supported`, - code: 1085, - severity: DiagnosticSeverity.Error + legacyCode: 1085, + severity: DiagnosticSeverity.Error, + code: 'fractional-hex-literal' }), unexpectedConditionalCompilationString: () => ({ - message: `Unexpected conditional-compilation string`, - code: 1086, - severity: DiagnosticSeverity.Error + message: `Unknown conditional compile keyword`, + legacyCode: 1086, + severity: DiagnosticSeverity.Error, + code: 'unknown-conditional-compile-keyword' }), duplicateConstDeclaration: (name: string) => ({ message: `Attempting to redeclare #const with name '${name}'`, - code: 1087, - severity: DiagnosticSeverity.Error + legacyCode: 1087, + severity: DiagnosticSeverity.Error, + code: 'duplicate-const-declaration' }), - constAliasDoesNotExist: (name: string) => ({ + __unused34: (name: string) => ({ message: `Attempting to create #const alias of '${name}', but no such #const exists`, - code: 1088, - severity: DiagnosticSeverity.Error + legacyCode: 1088, + severity: DiagnosticSeverity.Error, + code: 'const-alias-does-not-exist' }), invalidHashConstValue: () => ({ message: '#const declarations can only have values of `true`, `false`, or other #const names', - code: 1089, - severity: DiagnosticSeverity.Error + legacyCode: 1089, + severity: DiagnosticSeverity.Error, + code: 'invalid-hash-const-value' }), - referencedConstDoesNotExist: () => ({ + hashConstDoesNotExist: () => ({ message: `Referenced #const does not exist`, - code: 1090, - severity: DiagnosticSeverity.Error + legacyCode: 1090, + severity: DiagnosticSeverity.Error, + code: 'hash-const-does-not-exist' }), invalidHashIfValue: () => ({ message: `#if conditionals can only be 'true', 'false', or other #const names`, - code: 1091, - severity: DiagnosticSeverity.Error + legacyCode: 1091, + severity: DiagnosticSeverity.Error, + code: 'invalid-hash-if-value' }), + /** + * Treat #error directives like diagnostics, because the presence of `#error` even inside a runtime logical block that evaluates to false will 100% cause a compile error. + */ hashError: (message: string) => ({ - message: `#error ${message}`, - code: 1092, - severity: DiagnosticSeverity.Error + message: `#error ${message} `, + legacyCode: 1092, + severity: DiagnosticSeverity.Error, + code: 'hash-error' }), - expectedEqualAfterConstName: () => ({ + __unused43: () => ({ message: `Expected '=' after #const`, - code: 1093, - severity: DiagnosticSeverity.Error + legacyCode: 1093, + severity: DiagnosticSeverity.Error, + code: 'expected-equal-after-hash-const' }), - expectedHashEndIfToCloseHashIf: (startingLine: number) => ({ - message: `Expected '#end if' to close '#if' conditional compilation statement starting on line ${startingLine}`, - code: 1094, - severity: DiagnosticSeverity.Error + __unused17: (startingLine: number) => ({ + message: `Expected '#end if' to close '#if' conditional compilation statement starting on line ${startingLine} `, + legacyCode: 1094, + severity: DiagnosticSeverity.Error, + code: 'expected-terminator-on-hash-if' }), - constNameCannotBeReservedWord: () => ({ + __unused35: () => ({ message: `#const name cannot be a reserved word`, - code: 1095, - severity: DiagnosticSeverity.Error + legacyCode: 1095, + severity: DiagnosticSeverity.Error, + code: 'const-reservered-word' }), - expectedIdentifier: () => ({ + __unused22: () => ({ message: `Expected identifier`, - code: 1096, - severity: DiagnosticSeverity.Error + legacyCode: 1096, + severity: DiagnosticSeverity.Error, + code: 'expected-identifier' }), expectedAttributeNameAfterAtSymbol: () => ({ message: `Expected xml attribute name after '@'`, - code: 1097, - severity: DiagnosticSeverity.Error + legacyCode: 1097, + severity: DiagnosticSeverity.Error, + code: 'expected-attribute-name' }), childFieldTypeNotAssignableToBaseProperty: (childTypeName: string, baseTypeName: string, fieldName: string, childFieldType: string, parentFieldType: string) => ({ - message: `Field '${fieldName}' in class '${childTypeName}' is not assignable to the same field in base class '${baseTypeName}'. Type '${childFieldType}' is not assignable to type '${parentFieldType}'.`, - code: 1098, - severity: DiagnosticSeverity.Error + message: `Field '${fieldName}' in class '${childTypeName}' is not assignable to the same field in base class '${baseTypeName}'.Type '${childFieldType}' is not assignable to type '${parentFieldType}'.`, + legacyCode: 1098, + severity: DiagnosticSeverity.Error, + code: 'field-inheritance-mismatch' }), - classChildMemberDifferentMemberTypeThanAncestor: (memberType: string, parentMemberType: string, parentClassName: string) => ({ + __unused36: (memberType: string, parentMemberType: string, parentClassName: string) => ({ message: `Class member is a ${memberType} here but a ${parentMemberType} in ancestor class '${parentClassName}'`, - code: 1099, - severity: DiagnosticSeverity.Error + legacyCode: 1099, + severity: DiagnosticSeverity.Error, + code: 'child-field-type-different' }), classConstructorMissingSuperCall: () => ({ message: `Missing "super()" call in class constructor method.`, - code: 1100, - severity: DiagnosticSeverity.Error + legacyCode: 1100, + severity: DiagnosticSeverity.Error, + code: 'expected-super-call' }), classConstructorIllegalUseOfMBeforeSuperCall: () => ({ message: `Illegal use of "m" before calling "super()"`, - code: 1101, - severity: DiagnosticSeverity.Error + legacyCode: 1101, + severity: DiagnosticSeverity.Error, + code: 'expected-super-before-statement' }), classFieldCannotBeOverridden: () => ({ message: `Class field cannot be overridden`, - code: 1102, - severity: DiagnosticSeverity.Error + legacyCode: 1102, + severity: DiagnosticSeverity.Error, + code: 'invalid-field-override' }), unusedAnnotation: () => ({ message: `This annotation is not attached to any statement`, - code: 1103, - severity: DiagnosticSeverity.Error + legacyCode: 1103, + severity: DiagnosticSeverity.Error, + code: 'unexpected-annotation' }), localVarShadowedByScopedFunction: () => ({ - message: `Declaring a local variable with same name as scoped function can result in unexpected behavior`, - code: 1104, - severity: DiagnosticSeverity.Error + message: `Declaring a local variable with same name as scoped function or class can result in unexpected behavior`, + legacyCode: 1104, + severity: DiagnosticSeverity.Error, + code: 'var-shadows-function' }), scopeFunctionShadowedByBuiltInFunction: () => ({ - message: `Scope function will not be accessible because it has the same name as a built-in function`, - code: 1105, - severity: DiagnosticSeverity.Error + message: `Scope function will not be accessible because it has the same name as a built -in function`, + legacyCode: 1105, + severity: DiagnosticSeverity.Error, + code: 'native-function-collision' }), - localVarSameNameAsClass: (className: string) => ({ + __unused38: (className: string) => ({ message: `Local variable has same name as class '${className}'`, - code: 1106, - severity: DiagnosticSeverity.Error + legacyCode: 1106, + severity: DiagnosticSeverity.Error, + code: 'local-var-same-name-as-class' }), unnecessaryCodebehindScriptImport: () => ({ message: `This import is unnecessary because compiler option 'autoImportComponentScript' is enabled`, - code: 1107, - severity: DiagnosticSeverity.Warning + legacyCode: 1107, + severity: DiagnosticSeverity.Warning, + code: 'unnecessary-import' }), - expectedOpenParenToFollowCallfuncIdentifier: () => ({ + __unused37: () => ({ message: `Expected '(' to follow callfunc identifier`, - code: 1108, - severity: DiagnosticSeverity.Error + legacyCode: 1108, + severity: DiagnosticSeverity.Error, + code: 'expected-left-paren-after-callfunc' }), expectedToken: (...tokenKinds: string[]) => ({ message: `Expected token '${tokenKinds.join(`' or '`)}'`, - code: 1109, - severity: DiagnosticSeverity.Error + legacyCode: 1109, + severity: DiagnosticSeverity.Error, + code: 'expected-token' }), - __unused8: (paramName: string) => ({ + __unused10: (paramName: string) => ({ message: `Parameter '${paramName}' may not have the same name as namespace`, - code: 1110, - severity: DiagnosticSeverity.Error + legacyCode: 1110, + severity: DiagnosticSeverity.Error, + code: 'parameter-same-name-as-namespace' }), - __unused9: (variableName: string) => ({ + __unused11: (variableName: string) => ({ message: `Variable '${variableName}' may not have the same name as namespace`, - code: 1111, - severity: DiagnosticSeverity.Error + legacyCode: 1111, + severity: DiagnosticSeverity.Error, + code: 'variable-same-name-as-namespace' }), - unterminatedTemplateStringAtEndOfFile: () => ({ - message: `Unterminated template string at end of file`, - code: 1113, - severity: DiagnosticSeverity.Error + unterminatedTemplateString: () => ({ + message: `Unterminated template string`, + legacyCode: 1113, + severity: DiagnosticSeverity.Error, + code: 'unterminated-template-string' }), unterminatedTemplateExpression: () => ({ message: `Unterminated template string expression. '\${' must be followed by expression, then '}'`, - code: 1114, - severity: DiagnosticSeverity.Error + legacyCode: 1114, + severity: DiagnosticSeverity.Error, + code: 'unterminated-template-string-expression' }), duplicateComponentName: (componentName: string) => ({ message: `There are multiple components with the name '${componentName}'`, - code: 1115, - severity: DiagnosticSeverity.Error + legacyCode: 1115, + severity: DiagnosticSeverity.Error, + code: 'duplicate-component-name' }), __unused6: (className: string) => ({ message: `Function has same name as class '${className}'`, - code: 1116, + legacyCode: 1116, severity: DiagnosticSeverity.Error }), expectedExceptionVarToFollowCatch: () => ({ message: `Expected exception variable after 'catch' keyword`, - code: 1117, - severity: DiagnosticSeverity.Error + legacyCode: 1117, + severity: DiagnosticSeverity.Error, + code: 'expected-exception-variable' }), missingExceptionExpressionAfterThrowKeyword: () => ({ message: `Missing exception expression after 'throw' keyword`, - code: 1118, - severity: DiagnosticSeverity.Error + legacyCode: 1118, + severity: DiagnosticSeverity.Error, + code: 'expected-throw-expression' }), - missingLeftSquareBracketAfterDimIdentifier: () => ({ + __unused42: () => ({ message: `Missing left square bracket after 'dim' identifier`, - code: 1119, - severity: DiagnosticSeverity.Error + legacyCode: 1119, + severity: DiagnosticSeverity.Error, + code: 'expected-left-brace-after-dim' }), - missingRightSquareBracketAfterDimIdentifier: () => ({ + __unused39: () => ({ message: `Missing right square bracket after 'dim' identifier`, - code: 1120, - severity: DiagnosticSeverity.Error + legacyCode: 1120, + severity: DiagnosticSeverity.Error, + code: 'missing-right-brace-after-dim' }), missingExpressionsInDimStatement: () => ({ message: `Missing expression(s) in 'dim' statement`, - code: 1121, - severity: DiagnosticSeverity.Error + legacyCode: 1121, + severity: DiagnosticSeverity.Error, + code: 'expected-dim-expression' }), mismatchedOverriddenMemberVisibility: (childClassName: string, memberName: string, childAccessModifier: string, ancestorAccessModifier: string, ancestorClassName: string) => ({ message: `Access modifier mismatch: '${memberName}' is ${childAccessModifier} in type '${childClassName}' but is ${ancestorAccessModifier} in base type '${ancestorClassName}'.`, - code: 1122, - severity: DiagnosticSeverity.Error + legacyCode: 1122, + severity: DiagnosticSeverity.Error, + code: 'access-modifier-mismatch' }), __unused3: (typeName: string) => ({ message: `Cannot find type with name '${typeName}'`, - code: 1123, + legacyCode: 1123, severity: DiagnosticSeverity.Error }), enumValueMustBeType: (expectedType: string) => ({ message: `Enum value must be type '${expectedType}'`, - code: 1124, - severity: DiagnosticSeverity.Error + legacyCode: 1124, + severity: DiagnosticSeverity.Error, + code: 'enum-type-mismatch' }), enumValueIsRequired: (expectedType: string) => ({ message: `Value is required for ${expectedType} enum`, - code: 1125, - severity: DiagnosticSeverity.Error + legacyCode: 1125, + severity: DiagnosticSeverity.Error, + code: 'expected-enum-value' }), - unknownEnumValue: (name: string, enumName: string) => ({ + __unused40: (name: string, enumName: string) => ({ message: `Property '${name}' does not exist on enum '${enumName}'`, - code: 1126, - severity: DiagnosticSeverity.Error + legacyCode: 1126, + severity: DiagnosticSeverity.Error, + code: 'unknown-enum-value' }), __unused7: (scopeName: string, enumName: string) => ({ message: `Scope '${scopeName}' already contains an enum with name '${enumName}'`, - code: 1127, + legacyCode: 1127, severity: DiagnosticSeverity.Error }), unknownRoSGNode: (nodeName: string) => ({ message: `Unknown roSGNode '${nodeName}'`, - code: 1128, - severity: DiagnosticSeverity.Error + legacyCode: 1128, + severity: DiagnosticSeverity.Error, + code: 'unknown-rosgnode' }), unknownBrightScriptComponent: (componentName: string) => ({ message: `Unknown BrightScript component '${componentName}'`, - code: 1129, - severity: DiagnosticSeverity.Error + legacyCode: 1129, + severity: DiagnosticSeverity.Error, + code: 'unknown-brightscript-component' }), mismatchCreateObjectArgumentCount: (componentName: string, allowedArgCounts: number[], actualCount: number) => { const argCountArray = (allowedArgCounts || [1]).sort().filter((value, index, self) => self.indexOf(value) === index); return { message: `For ${componentName}, expected ${argCountArray.map(c => c.toString()).join(' or ')} total arguments, but got ${actualCount}.`, - code: 1130, - severity: DiagnosticSeverity.Error + legacyCode: 1130, + severity: DiagnosticSeverity.Error, + code: 'incorrect-createobject-argument-count' }; }, - deprecatedBrightScriptComponent: (componentName: string, deprecatedDescription?: string) => ({ - message: `${componentName} has been deprecated${deprecatedDescription ? ': ' + deprecatedDescription : ''}`, - code: 1131, - severity: DiagnosticSeverity.Error + __unused41: (componentName: string, deprecatedDescription?: string) => ({ + message: `${componentName} has been deprecated${deprecatedDescription ? ': ' + deprecatedDescription : ''} `, + legacyCode: 1131, + severity: DiagnosticSeverity.Error, + code: 'deprecated-brightscript-component' }), circularReferenceDetected: (items: string[], scopeName: string) => ({ - message: `Circular reference detected between ${Array.isArray(items) ? items.join(' -> ') : ''} in scope '${scopeName}'`, - code: 1132, - severity: DiagnosticSeverity.Error + message: `Circular inheritance detected between ${Array.isArray(items) ? items.join(' -> ') : ''} in scope '${scopeName}'`, + legacyCode: 1132, + severity: DiagnosticSeverity.Error, + code: 'circular-inheritance' }), unexpectedStatementOutsideFunction: () => ({ message: `Unexpected statement found outside of function body`, - code: 1133, - severity: DiagnosticSeverity.Error + legacyCode: 1133, + severity: DiagnosticSeverity.Error, + code: 'unexpected-statement' }), detectedTooDeepFileSource: (numberOfParentDirectories: number) => ({ - message: `Expected directory depth no larger than 7, but found ${numberOfParentDirectories}`, - code: 1134, - severity: DiagnosticSeverity.Error + message: `Expected directory depth no larger than 7, but found ${numberOfParentDirectories} `, + legacyCode: 1134, + severity: DiagnosticSeverity.Error, + code: 'directory-depth' }), illegalContinueStatement: () => ({ message: `Continue statement must be contained within a loop statement`, - code: 1135, - severity: DiagnosticSeverity.Error + legacyCode: 1135, + severity: DiagnosticSeverity.Error, + code: 'unexpected-continue' }), keywordMustBeDeclaredAtNamespaceLevel: (keyword: string) => ({ message: `${keyword} must be declared at the root level or within a namespace`, - code: 1136, - severity: DiagnosticSeverity.Error + legacyCode: 1136, + severity: DiagnosticSeverity.Error, + code: 'invalid-declaration-location' }), itemCannotBeUsedAsVariable: (itemType: string) => ({ message: `${itemType} cannot be used as a variable`, - code: 1137, - severity: DiagnosticSeverity.Error + legacyCode: 1137, + severity: DiagnosticSeverity.Error, + code: 'type-not-variable' }), callfuncHasToManyArgs: (numberOfArgs: number) => ({ - message: `You can not have more than 5 arguments in a callFunc. ${numberOfArgs} found.`, - code: 1138, - severity: DiagnosticSeverity.Error + message: `You can not have more than 5 arguments in a callFunc.${numberOfArgs} found.`, + legacyCode: 1138, + severity: DiagnosticSeverity.Error, + code: 'exceeds-max-callfunc-arg-count' }), noOptionalChainingInLeftHandSideOfAssignment: () => ({ message: `Optional chaining may not be used in the left-hand side of an assignment`, - code: 1139, - severity: DiagnosticSeverity.Error + legacyCode: 1139, + severity: DiagnosticSeverity.Error, + code: 'unexpected-optional-chain' }), /** * @@ -738,90 +938,99 @@ export let DiagnosticMessages = { * @param typeDescriptor defaults to 'type' ... could also be 'namespace', etc. */ cannotFindFunction: (name: string, fullName?: string, typeName?: string, typeDescriptor = 'type') => ({ - message: `Cannot find function '${name}'${typeName ? ` for ${typeDescriptor} '${typeName}'` : ''}`, - code: 1140, + message: `Cannot find function '${name}'${typeName ? ` for ${typeDescriptor} '${typeName}'` : ''} `, + legacyCode: 1140, data: { name: name, fullName: fullName ?? name, typeName: typeName ? typeName : undefined }, - severity: DiagnosticSeverity.Error + severity: DiagnosticSeverity.Error, + code: 'cannot-find-function' }), argumentTypeMismatch: (actualTypeString: string, expectedTypeString: string, data?: TypeCompatibilityData) => ({ - message: `Argument of type '${actualTypeString}' is not compatible with parameter of type '${expectedTypeString}'${typeCompatibilityMessage(actualTypeString, expectedTypeString, data)}`, + message: `Argument of type '${actualTypeString}' is not compatible with parameter of type '${expectedTypeString}'${typeCompatibilityMessage(actualTypeString, expectedTypeString, data)} `, data: data, - code: 1141, - severity: DiagnosticSeverity.Error + legacyCode: 1141, + severity: DiagnosticSeverity.Error, + code: 'argument-type-mismatch' }), returnTypeMismatch: (actualTypeString: string, expectedTypeString: string, data?: TypeCompatibilityData) => ({ - message: `Type '${actualTypeString}' is not compatible with declared return type '${expectedTypeString}'${typeCompatibilityMessage(actualTypeString, expectedTypeString, data)}'`, + message: `Type '${actualTypeString}' is not compatible with declared return type '${expectedTypeString}'${typeCompatibilityMessage(actualTypeString, expectedTypeString, data)} '`, data: data, - code: 1142, - severity: DiagnosticSeverity.Error + legacyCode: 1142, + severity: DiagnosticSeverity.Error, + code: 'return-type-mismatch' }), assignmentTypeMismatch: (actualTypeString: string, expectedTypeString: string, data?: TypeCompatibilityData) => ({ message: `Type '${actualTypeString}' is not compatible with type '${expectedTypeString}'${typeCompatibilityMessage(actualTypeString, expectedTypeString, data)}`, data: data, - code: 1143, - severity: DiagnosticSeverity.Error + legacyCode: 1143, + severity: DiagnosticSeverity.Error, + code: 'assignment-type-mismatch' }), operatorTypeMismatch: (operatorString: string, firstType: string, secondType = '') => ({ message: `Operator '${operatorString}' cannot be applied to type${secondType ? 's' : ''} '${firstType}'${secondType ? ` and '${secondType}'` : ''}`, - code: 1144, - severity: DiagnosticSeverity.Error + legacyCode: 1144, + severity: DiagnosticSeverity.Error, + code: 'operator-type-mismatch' }), incompatibleSymbolDefinition: (symbol: string, scopeName: string) => ({ message: `'${symbol}' is incompatible across these scopes: ${scopeName}`, - code: 1145, - severity: DiagnosticSeverity.Error - }), - memberAccessibilityMismatch: (memberName: string, accessModifierFlag: SymbolTypeFlag, definingClassName: string) => ({ - message: `Member '${memberName}' is ${accessModifierNameFromFlag(accessModifierFlag)}${accessModifierAdditionalInfo(accessModifierFlag, definingClassName)}`, // TODO: Add scopes where it was defined - code: 1146, - severity: DiagnosticSeverity.Error - }), - typecastStatementMustBeDeclaredAtStart: () => ({ - message: `'typecast' statement must be declared at the top of the file or beginning of function or namespace`, - code: 1147, - severity: DiagnosticSeverity.Error + legacyCode: 1145, + severity: DiagnosticSeverity.Error, + code: 'incompatible-definition' }), + memberAccessibilityMismatch: (memberName: string, accessModifierFlag: SymbolTypeFlag, definingClassName: string) => { + let accessModName: string = TokenKind.Public; + // eslint-disable-next-line no-bitwise + if (accessModifierFlag & SymbolTypeFlag.private) { + accessModName = TokenKind.Private; + // eslint-disable-next-line no-bitwise + } else if (accessModifierFlag & SymbolTypeFlag.protected) { + accessModName = TokenKind.Protected; + } + accessModName = accessModName.toLowerCase(); + let accessAdditionalInfo = ''; + + // eslint-disable-next-line no-bitwise + if (accessModifierFlag & SymbolTypeFlag.private) { + accessAdditionalInfo = ` and only accessible from within class '${definingClassName}'`; + // eslint-disable-next-line no-bitwise + } else if (accessModifierFlag & SymbolTypeFlag.protected) { + accessAdditionalInfo = ` and only accessible from within class '${definingClassName}' and its subclasses`; + } + + return { + message: `Member '${memberName}' is ${accessModName}${accessAdditionalInfo}`, // TODO: Add scopes where it was defined + legacyCode: 1146, + severity: DiagnosticSeverity.Error, + code: 'member-access-violation' + }; + }, invalidTypecastStatementApplication: (foundApplication: string) => ({ message: `'typecast' statement can only be applied to 'm', but was applied to '${foundApplication}'`, - code: 1148, - severity: DiagnosticSeverity.Error + legacyCode: 1148, + severity: DiagnosticSeverity.Error, + code: 'invalid-typecast-target' }), itemCannotBeUsedAsType: (typeText: string) => ({ message: `'${typeText}' cannot be used as a type`, - code: 1149, - severity: DiagnosticSeverity.Error - }), - expectedNewlineInConditionalCompile: () => ({ - message: `Expected newline in conditional compilation statement`, - code: 1151, - severity: DiagnosticSeverity.Error - }), - expectedTerminatorOnConditionalCompileBlock: () => ({ - message: `Expected '#end if', '#else if', or '#else' to terminate conditional compilation block`, - code: 1152, - severity: DiagnosticSeverity.Error + legacyCode: 1149, + severity: DiagnosticSeverity.Error, + code: 'invalid-type-reference' }), unsafeUnmatchedTerminatorInConditionalCompileBlock: (terminator: string) => ({ message: `Unsafe unmatched terminator '${terminator}' in conditional compilation block`, - code: 1153, - severity: DiagnosticSeverity.Error - }), - cannotFindTypeInCommentDoc: (name: string) => ({ - message: `Cannot find type '${name}' in doc comment`, - code: 1154, - data: { - name: name - }, - severity: DiagnosticSeverity.Error + legacyCode: 1150, + severity: DiagnosticSeverity.Error, + code: 'inconsistent-conditional-compile-nesting' }), expectedReturnStatement: () => ({ message: `Expected return statement in function`, - code: 1155, - severity: DiagnosticSeverity.Error + legacyCode: 1151, + severity: DiagnosticSeverity.Error, + code: 'expected-return-statement' }) }; export const defaultMaximumTruncationLength = 160; @@ -852,39 +1061,39 @@ export function typeCompatibilityMessage(actualTypeString: string, expectedTypeS return message; } -function accessModifierNameFromFlag(accessModifierFlag: SymbolTypeFlag) { - let result = TokenKind.Public; - // eslint-disable-next-line no-bitwise - if (accessModifierFlag & SymbolTypeFlag.private) { - result = TokenKind.Private; - // eslint-disable-next-line no-bitwise - } else if (accessModifierFlag & SymbolTypeFlag.protected) { - result = TokenKind.Protected; +function getPossibilitiesString(possibilities: string[] | string) { + if (!Array.isArray(possibilities)) { + return `'${possibilities}'`; } - return result.toLowerCase(); -} + if (possibilities.length === 1) { + return `'${possibilities}'`; -function accessModifierAdditionalInfo(accessModifierFlag: SymbolTypeFlag, className: string) { - // eslint-disable-next-line no-bitwise - if (accessModifierFlag & SymbolTypeFlag.private) { - return ` and only accessible from within class '${className}'`; - // eslint-disable-next-line no-bitwise - } else if (accessModifierFlag & SymbolTypeFlag.protected) { - return ` and only accessible from within class '${className}' and its subclasses`; } - return TokenKind.Public; + let result = ''; + for (let i = 0; i < possibilities.length; i++) { + result += `'${possibilities[i]}'`; + if (i < possibilities.length - 2) { + result += ', '; + } else if (i < possibilities.length - 1) { + result += ' or '; + } + } + return result; } -export const DiagnosticCodeMap = {} as Record; -export let diagnosticCodes = [] as number[]; +export const DiagnosticCodeMap = {} as Record; +export const DiagnosticLegacyCodeMap = {} as Record; +export let diagnosticCodes = [] as string[]; for (let key in DiagnosticMessages) { diagnosticCodes.push(DiagnosticMessages[key]().code); + diagnosticCodes.push(DiagnosticMessages[key]().legacyCode); DiagnosticCodeMap[key] = DiagnosticMessages[key]().code; + DiagnosticLegacyCodeMap[key] = DiagnosticMessages[key]().legacyCode; } export interface DiagnosticInfo { message: string; - code: number; + legacyCode: number; severity: DiagnosticSeverity; } diff --git a/src/DiagnosticSeverityAdjuster.ts b/src/DiagnosticSeverityAdjuster.ts index b651f30bf..d5ef5afb7 100644 --- a/src/DiagnosticSeverityAdjuster.ts +++ b/src/DiagnosticSeverityAdjuster.ts @@ -11,6 +11,10 @@ export class DiagnosticSeverityAdjuster { if (map.has(code)) { diagnostic.severity = map.get(code); } + const legacyCode = String(diagnostic.legacyCode); + if (legacyCode && map.has(legacyCode)) { + diagnostic.severity = map.get(legacyCode); + } }); } diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts index 371576126..0500aebb3 100644 --- a/src/LanguageServer.ts +++ b/src/LanguageServer.ts @@ -37,7 +37,6 @@ import { URI } from 'vscode-uri'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type { BsConfig } from './BsConfig'; import { Deferred } from './deferred'; -import { DiagnosticMessages } from './DiagnosticMessages'; import { ProgramBuilder } from './ProgramBuilder'; import { standardizePath as s, util } from './util'; import { Throttler } from './Throttler'; @@ -595,14 +594,6 @@ export class LanguageServer { newProject.isFirstRunComplete = true; newProject.isFirstRunSuccessful = false; } - //if we found a deprecated brsconfig.json, add a diagnostic warning the user - if (configFilePath && path.basename(configFilePath) === 'brsconfig.json') { - builder.addDiagnostic(configFilePath, { - ...DiagnosticMessages.brsConfigJsonIsDeprecated(), - location: util.createLocationFromRange(util.pathToUri(configFilePath), util.createRange(0, 0, 0, 0)) - }); - return this.sendDiagnostics(); - } } private async createStandaloneFileProject(srcPath: string) { diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 3519a5ca0..50ef5cf3b 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -1138,10 +1138,13 @@ describe('Program', () => { `); //the file should be included in the program expect(program.getFile('components/a/b/c/main.brs')).to.exist; - let diagnostics = program.getDiagnostics(); - expectHasDiagnostics(diagnostics); - let parseError = diagnostics.filter(x => x.message === 'Unterminated string at end of line')[0]; - expect(parseError).to.exist; + expectDiagnostics(program, [ + DiagnosticMessages.expectedStatement(), + { + ...DiagnosticMessages.unterminatedString(), + location: util.createLocation(2, 20, 2, 49, s`${rootDir}/components/a/b/c/main.brs`) + } + ]); }); it('it excludes specified error codes', () => { @@ -2702,7 +2705,7 @@ describe('Program', () => { //the buffers should be identical expect( - data.compare(result) + data.compare(result as any) ).to.equal(0); }); diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index 311a097f5..4093e116b 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -1011,11 +1011,14 @@ describe('Scope', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.nameCollision('Function', 'Global Function', 'Str').message, { + message: DiagnosticMessages.nameCollision('Function', 'Global Function', 'Str').message, + location: { range: Range.create(4, 29, 4, 32) } + }, { message: DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction().message, location: { range: Range.create(4, 29, 4, 32) } - }]); + } + ]); }); }); @@ -4278,7 +4281,7 @@ describe('Scope', () => { end sub `); program.validate(); - expectDiagnosticsIncludes(program, DiagnosticMessages.localVarSameNameAsClass('Person').message); + expectDiagnosticsIncludes(program, DiagnosticMessages.localVarShadowedByScopedFunction().message); }); it('disallows reusing a class name as "for each" variable in a method', () => { @@ -4295,7 +4298,7 @@ describe('Scope', () => { end class `); program.validate(); - expectDiagnosticsIncludes(program, DiagnosticMessages.localVarSameNameAsClass('Person').message); + expectDiagnosticsIncludes(program, DiagnosticMessages.localVarShadowedByScopedFunction().message); }); it('allows reusing a namespaced class name as "for each" variable in a method', () => { diff --git a/src/XmlScope.spec.ts b/src/XmlScope.spec.ts index 29421ae83..1517494f1 100644 --- a/src/XmlScope.spec.ts +++ b/src/XmlScope.spec.ts @@ -129,7 +129,7 @@ describe('XmlScope', () => { ...DiagnosticMessages.xmlTagMissingAttribute('function', 'name'), location: { range: Range.create(7, 9, 7, 17) } }, { // syntax error expecting '=' but found '/>' - code: DiagnosticMessages.xmlGenericParseError('').code + code: DiagnosticMessages.syntaxError('').code }, { // onChange function ...DiagnosticMessages.xmlFunctionNotFound('func4'), location: { range: Range.create(8, 51, 8, 56) } @@ -178,7 +178,7 @@ describe('XmlScope', () => { ...DiagnosticMessages.xmlTagMissingAttribute('field', 'type'), location: { range: Range.create(9, 9, 9, 14) } }, { // syntax error expecting '=' but found '/>' - code: DiagnosticMessages.xmlGenericParseError('').code + code: DiagnosticMessages.syntaxError('').code }]); }); }); diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.ts index ed330a6c7..23157d88c 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.ts @@ -21,8 +21,6 @@ export class CodeActionsProcessor { for (const diagnostic of this.event.diagnostics) { if (diagnostic.code === DiagnosticCodeMap.cannotFindName || diagnostic.code === DiagnosticCodeMap.cannotFindFunction) { this.suggestCannotFindName(diagnostic as any); - } else if (diagnostic.code === DiagnosticCodeMap.classCouldNotBeFound) { - this.suggestClassImports(diagnostic as any); } else if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) { this.addMissingExtends(diagnostic as any); } @@ -86,20 +84,6 @@ export class CodeActionsProcessor { ); } - private suggestClassImports(diagnostic: DiagnosticMessageType<'classCouldNotBeFound'>) { - //skip if not a BrighterScript file - const file = this.event.program.getFile(diagnostic.location?.uri); - if (!file || (file as BrsFile).parseMode !== ParseMode.BrighterScript) { - return; - } - const lowerClassName = diagnostic.data.className.toLowerCase(); - this.suggestImports( - diagnostic, - lowerClassName, - this.event.program.findFilesForClass(lowerClassName) - ); - } - private addMissingExtends(diagnostic: DiagnosticMessageType<'xmlComponentMissingExtendsAttribute'>) { const srcPath = this.event.file.srcPath; const { componentElement } = (this.event.file as XmlFile).parser.ast; diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 802f7464b..c17f01369 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -354,8 +354,8 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.typecastStatementMustBeDeclaredAtStart().message, - DiagnosticMessages.typecastStatementMustBeDeclaredAtStart().message + DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace').message, + DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace').message ]); }); @@ -393,7 +393,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.typecastStatementMustBeDeclaredAtStart().message + DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace').message ]); }); @@ -425,7 +425,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.typecastStatementMustBeDeclaredAtStart().message + DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace').message ]); }); @@ -636,8 +636,8 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias').message, - DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias').message + DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file').message, + DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file').message ]); }); @@ -995,7 +995,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('TypeNotThere').message + DiagnosticMessages.cannotFindName('TypeNotThere').message ]); const data = {} as ExtraSymbolData; expectTypeToBe( @@ -1065,7 +1065,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('TypeNotThere').message + DiagnosticMessages.cannotFindName('TypeNotThere').message ]); const data = {} as ExtraSymbolData; const funcStmt = file.ast.findChild(isFunctionStatement); @@ -1172,7 +1172,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('unknown').message + DiagnosticMessages.cannotFindName('unknown').message ]); const data = {} as ExtraSymbolData; const funcStmt = file.ast.findChild(isFunctionStatement); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 57059e2c3..e4c3ff0f4 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -450,7 +450,7 @@ export class BrsFileValidator { const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False; if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.referencedConstDoesNotExist(), + ...DiagnosticMessages.hashConstDoesNotExist(), location: ccConst.location }); return false; @@ -523,17 +523,17 @@ export class BrsFileValidator { if (!topOfFileStatements.includes(result)) { if (isLibraryStatement(result)) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library'), + ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'), location: result.location }); } else if (isImportStatement(result)) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('import'), + ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'), location: result.location }); } else if (isAliasStatement(result)) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias'), + ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'), location: result.location }); } @@ -548,7 +548,7 @@ export class BrsFileValidator { for (let i = 1; i < topOfFileTypecastStatements.length; i++) { const typecastStmt = topOfFileTypecastStatements[i]; this.event.program.diagnostics.register({ - ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(), + ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'), location: typecastStmt.location }); } @@ -579,7 +579,7 @@ export class BrsFileValidator { if (!isFirst || !isAllowedBlock) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(), + ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'), location: result.location }); } diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 64c9a3399..4b9577c95 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -3330,7 +3330,7 @@ describe('ScopeValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('TypeNotThere').message + DiagnosticMessages.cannotFindName('TypeNotThere').message ]); }); @@ -3343,7 +3343,7 @@ describe('ScopeValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('TypeNotThere').message + DiagnosticMessages.cannotFindName('TypeNotThere').message ]); }); @@ -3356,7 +3356,7 @@ describe('ScopeValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindTypeInCommentDoc('TypeNotThere').message + DiagnosticMessages.cannotFindName('TypeNotThere').message ]); }); diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 59940a741..755adb9ae 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -332,7 +332,7 @@ export class ScopeValidator { // Test for deprecation if (brightScriptComponent?.isDeprecated) { this.addDiagnostic({ - ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription), + ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription), location: call.location }); } @@ -768,10 +768,11 @@ export class ScopeValidator { } } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) { const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj)); + const typeChainScanForItem = util.processTypeChain(typeChain); const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1)); if (enumFileLink) { this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.unknownEnumValue(lastTypeInfo?.name, typeChainScanForParent.fullChainName), + ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'), location: lastTypeInfo?.location, relatedInformation: [{ message: 'Enum declared here', @@ -1116,7 +1117,7 @@ export class ScopeValidator { const classStmtLink = this.event.scope.getClassFileLink(lowerVarName); if (classStmtLink) { this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.localVarSameNameAsClass(classStmtLink?.item?.getName(ParseMode.BrighterScript)), + ...DiagnosticMessages.localVarShadowedByScopedFunction(), location: util.createLocationFromFileRange(file, varDeclaration.nameRange), relatedInformation: [{ message: 'Class declared here', @@ -1196,7 +1197,7 @@ export class ScopeValidator { const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime }); if (!foundType?.isResolvable()) { this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.cannotFindTypeInCommentDoc(docTypeTag.typeString), + ...DiagnosticMessages.cannotFindName(docTypeTag.typeString), location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location }); } diff --git a/src/diagnosticUtils.ts b/src/diagnosticUtils.ts index 0fb043b3b..ddf269a83 100644 --- a/src/diagnosticUtils.ts +++ b/src/diagnosticUtils.ts @@ -72,6 +72,8 @@ export function printDiagnostic( let severityText = severityTextMap[severity]; console.log(''); + const printableDiagnosticCode = diagnostic.code ? diagnostic.code.toString() : 'BS' + ((diagnostic as BsDiagnostic).legacyCode ?? ''); + console.log( chalk.cyan(filePath ?? '') + ':' + @@ -83,7 +85,7 @@ export function printDiagnostic( ' - ' + typeColor[severity](severityText) + ' ' + - chalk.grey('BS' + diagnostic.code) + + chalk.grey(printableDiagnosticCode) + ': ' + chalk.white(diagnostic.message) ); diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts index 6a285907a..2227241e6 100644 --- a/src/files/BrsFile.Class.spec.ts +++ b/src/files/BrsFile.Class.spec.ts @@ -1281,20 +1281,6 @@ describe('BrsFile BrighterScript classes', () => { ]); }); - it.skip('detects calls to unknown m methods', () => { - program.setFile('source/main.bs', ` - class Animal - sub new() - m.methodThatDoesNotExist() - end sub - end class - `); - program.validate(); - expectDiagnostics(program, [ - DiagnosticMessages.methodDoesNotExistOnType('methodThatDoesNotExist', 'Animal') - ]); - }); - it('detects direct circular extends', () => { //direct program.setFile('source/Direct.bs', ` @@ -1431,7 +1417,7 @@ describe('BrsFile BrighterScript classes', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.classChildMemberDifferentMemberTypeThanAncestor('method', 'field', 'Animal') + DiagnosticMessages.childFieldTypeNotAssignableToBaseProperty('Duck', 'Animal', 'name', 'function name() as dynamic', 'dynamic') ]); }); @@ -1789,7 +1775,7 @@ describe('BrsFile BrighterScript classes', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.localVarSameNameAsClass('Animal').message + DiagnosticMessages.localVarShadowedByScopedFunction().message ]); }); diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 18d6e5ffc..7a72616b1 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -434,6 +434,18 @@ describe('BrsFile', () => { }]); }); + it('recognizes diagnostic names', () => { + let file = program.setFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, ` + sub Main() + 'bs:disable-next-line: cannot-find-name + name = unknown + end sub + `); + expect(file.commentFlags[0]).to.exist; + program.validate(); + expectZeroDiagnostics(program); + }); + }); describe('bs:disable-line', () => { @@ -732,7 +744,7 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.referencedConstDoesNotExist() + DiagnosticMessages.hashConstDoesNotExist() ]); }); @@ -746,7 +758,7 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.referencedConstDoesNotExist() + DiagnosticMessages.hashConstDoesNotExist() ]); }); @@ -771,7 +783,7 @@ describe('BrsFile', () => { end sub `); expectDiagnostics(program, [ - DiagnosticMessages.constNameCannotBeReservedWord() + DiagnosticMessages.cannotUseReservedWordAsIdentifier('function') ]); }); @@ -1179,7 +1191,7 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('import') + DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file') ]); }); @@ -1198,7 +1210,7 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library') + DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file') ]); }); @@ -1210,7 +1222,7 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library') + DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file') ]); }); @@ -1319,10 +1331,10 @@ describe('BrsFile', () => { end function `); expectDiagnostics(file.parser.diagnostics, [ - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(), + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments'), DiagnosticMessages.expectedNewlineOrColon(), DiagnosticMessages.unexpectedToken('end function'), - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(), + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments'), DiagnosticMessages.expectedNewlineOrColon() ]); }); @@ -4846,10 +4858,10 @@ describe('BrsFile', () => { `); program.validate(); expectDiagnostics(program, [{ - ...DiagnosticMessages.mismatchedEndCallableKeyword('function', 'sub'), + ...DiagnosticMessages.closingKeywordMismatch('function', 'sub'), location: util.createLocationFromFileRange(file, util.createRange(2, 12, 2, 19)) }, { - ...DiagnosticMessages.mismatchedEndCallableKeyword('sub', 'function'), + ...DiagnosticMessages.closingKeywordMismatch('sub', 'function'), location: util.createLocationFromFileRange(file, util.createRange(4, 12, 4, 24)) }]); }); diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 1aee5d54e..d0a331f3e 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -6,7 +6,7 @@ import { CompletionItemKind } from 'vscode-languageserver'; import chalk from 'chalk'; import * as path from 'path'; import { Scope } from '../Scope'; -import { DiagnosticCodeMap, diagnosticCodes, DiagnosticMessages } from '../DiagnosticMessages'; +import { DiagnosticCodeMap, diagnosticCodes, DiagnosticLegacyCodeMap, DiagnosticMessages } from '../DiagnosticMessages'; import { FunctionScope } from '../FunctionScope'; import type { Callable, CallableParam, CommentFlag, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces'; import type { Token } from '../lexer/Token'; @@ -442,7 +442,7 @@ export class BrsFile implements BscFile { * @param tokens - an array of tokens of which to find `TokenKind.Comment` from */ public getCommentFlags(tokens: Token[]) { - const processor = new CommentFlagProcessor(this, ['rem', `'`], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode]); + const processor = new CommentFlagProcessor(this, ['rem', `'`], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode, DiagnosticLegacyCodeMap.unknownDiagnosticCode]); this.commentFlags = []; for (let lexerToken of tokens) { diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts index ed494fb66..061ea7f16 100644 --- a/src/files/XmlFile.spec.ts +++ b/src/files/XmlFile.spec.ts @@ -160,10 +160,10 @@ describe('XmlFile', () => { ` ); expectDiagnostics(program, [{ // expecting opening tag but got prolog - code: DiagnosticMessages.xmlGenericParseError('').code, + code: DiagnosticMessages.syntaxError('').code, location: { range: Range.create(1, 16, 1, 22) } }, { - ...DiagnosticMessages.xmlGenericParseError('Syntax error: whitespace found before the XML prolog'), + ...DiagnosticMessages.syntaxError('Syntax error: whitespace found before the XML prolog'), location: { range: Range.create(0, 0, 1, 16) } }]); }); @@ -223,7 +223,7 @@ describe('XmlFile', () => { program.validate(); const diagnostics = program.getDiagnostics(); expect(diagnostics).to.be.lengthOf(2); - expect(diagnostics[0].code).to.equal(DiagnosticMessages.xmlGenericParseError('').code); //unexpected character '1' + expect(diagnostics[0].code).to.equal(DiagnosticMessages.syntaxError('').code); //unexpected character '1' expect(diagnostics[1]).to.deep.include({ code: DiagnosticMessages.xmlComponentMissingNameAttribute().code, location: util.createLocationFromFileRange(file, Range.create(1, 1, 1, 10)) diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts index d1e2aeac6..56d8de497 100644 --- a/src/files/XmlFile.ts +++ b/src/files/XmlFile.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import type { CodeWithSourceMap } from 'source-map'; import { SourceNode } from 'source-map'; import type { Location, Position, Range } from 'vscode-languageserver'; -import { DiagnosticCodeMap, diagnosticCodes } from '../DiagnosticMessages'; +import { DiagnosticCodeMap, DiagnosticLegacyCodeMap, diagnosticCodes } from '../DiagnosticMessages'; import type { Callable, FileReference, CommentFlag, SerializedCodeFile } from '../interfaces'; import type { Program } from '../Program'; import util from '../util'; @@ -279,7 +279,7 @@ export class XmlFile implements BscFile { * Collect all bs: comment flags */ public getCommentFlags(tokens: Array) { - const processor = new CommentFlagProcessor(this, [' OPEN <-- but found --> \' { if x print 1 else if y : print 2 `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock().message + DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block').message ]); }); diff --git a/src/parser/tests/expression/ArrayLiterals.spec.ts b/src/parser/tests/expression/ArrayLiterals.spec.ts index d4f5ef686..8ef980d81 100644 --- a/src/parser/tests/expression/ArrayLiterals.spec.ts +++ b/src/parser/tests/expression/ArrayLiterals.spec.ts @@ -180,7 +180,7 @@ describe('parser array literals', () => { // no closing brace: let { ast, diagnostics } = Parser.parse(`_ = [1, data.foo`); - expectDiagnostics(diagnostics, [DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral()]); + expectDiagnostics(diagnostics, [DiagnosticMessages.unmatchedLeftToken('[', 'array literal')]); expect(ast.statements).to.be.lengthOf(1); expect(isAssignmentStatement(ast.statements[0])).to.be.true; const assignStmt = ast.statements[0] as AssignmentStatement; @@ -197,7 +197,7 @@ describe('parser array literals', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral() + DiagnosticMessages.unmatchedLeftToken('[', 'array literal') ]); }); @@ -208,7 +208,7 @@ describe('parser array literals', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral() + DiagnosticMessages.unmatchedLeftToken('[', 'array literal') ]); }); }); diff --git a/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts b/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts index 646c9d129..194cf4199 100644 --- a/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts +++ b/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts @@ -210,7 +210,7 @@ describe('parser associative array literals', () => { it('will still be parsed', () => { // No closing brace: let { ast, diagnostics } = Parser.parse(`_ = {name: "john", age: 42, address: data.address`); - expectDiagnostics(diagnostics, [DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral()]); + expectDiagnostics(diagnostics, [DiagnosticMessages.unmatchedLeftToken('{', 'associative array literal')]); expect(ast.statements).to.be.lengthOf(1); expect(isAssignmentStatement(ast.statements[0])).to.be.true; const assignStmt = ast.statements[0] as AssignmentStatement; @@ -231,7 +231,7 @@ describe('parser associative array literals', () => { `); expectDiagnostics(diagnostics, [ DiagnosticMessages.unexpectedToken('\n'), - DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral() + DiagnosticMessages.unmatchedLeftToken('{', 'associative array literal') ]); }); @@ -243,7 +243,7 @@ describe('parser associative array literals', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral() + DiagnosticMessages.unmatchedLeftToken('{', 'associative array literal') ]); }); }); diff --git a/src/parser/tests/expression/Call.spec.ts b/src/parser/tests/expression/Call.spec.ts index 82b95bdc1..99ed2129a 100644 --- a/src/parser/tests/expression/Call.spec.ts +++ b/src/parser/tests/expression/Call.spec.ts @@ -56,7 +56,7 @@ describe('parser call expressions', () => { //there should only be 1 error expectDiagnostics(diagnostics, [ DiagnosticMessages.unexpectedToken(':'), - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments() + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments') ]); expect(ast.statements).to.be.length.greaterThan(0); //the error should be BEFORE the `name = "bob"` statement @@ -89,10 +89,10 @@ describe('parser call expressions', () => { expect(diagnostics).to.be.lengthOf(4); expectDiagnostics(diagnostics, [ - DiagnosticMessages.expectedPropertyNameAfterPeriod(), - DiagnosticMessages.expectedPropertyNameAfterPeriod(), - DiagnosticMessages.expectedPropertyNameAfterPeriod(), - DiagnosticMessages.expectedPropertyNameAfterPeriod() + DiagnosticMessages.expectedIdentifier(), + DiagnosticMessages.expectedIdentifier(), + DiagnosticMessages.expectedIdentifier(), + DiagnosticMessages.expectedIdentifier() ]); expect(ast.statements).to.be.lengthOf(1); const bodyStatements = (ast.statements[0] as FunctionStatement).func.body.statements; @@ -218,7 +218,7 @@ describe('parser call expressions', () => { expect(diagnostics).to.be.lengthOf(2); expectDiagnostics(diagnostics, [ - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(), + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments'), DiagnosticMessages.expectedNewlineOrColon() ]); expect(ast.statements).to.be.lengthOf(1); @@ -244,7 +244,7 @@ describe('parser call expressions', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments() + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments') ]); }); @@ -255,7 +255,7 @@ describe('parser call expressions', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments() + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments') ]); }); @@ -266,7 +266,7 @@ describe('parser call expressions', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedRightParenAfterFunctionCallArguments() + DiagnosticMessages.unmatchedLeftToken('(', 'function call arguments') ]); expect(ast.statements).to.be.lengthOf(1); const bodyStatements = (ast.statements[0] as FunctionStatement).func.body.statements; diff --git a/src/parser/tests/expression/Indexing.spec.ts b/src/parser/tests/expression/Indexing.spec.ts index 41209049d..fb8d7ec1e 100644 --- a/src/parser/tests/expression/Indexing.spec.ts +++ b/src/parser/tests/expression/Indexing.spec.ts @@ -81,7 +81,7 @@ describe('parser indexing', () => { expect(diagnostics.length).to.equal(3); expectDiagnostics(diagnostics, [ - DiagnosticMessages.expectedPropertyNameAfterPeriod(), // expected name after first dot + DiagnosticMessages.expectedIdentifier(), // expected name after first dot DiagnosticMessages.expectedNewlineOrColon(), // expected newline after "_ = foo" statement DiagnosticMessages.unexpectedToken('.') // everything after the 2nd dot is ignored ]); @@ -307,7 +307,7 @@ describe('parser indexing', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex() + DiagnosticMessages.unmatchedLeftToken('[', 'array or object index') ]); }); @@ -318,7 +318,7 @@ describe('parser indexing', () => { end sub `); expectDiagnosticsIncludes(diagnostics, [ - DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex() + DiagnosticMessages.unmatchedLeftToken('[', 'array or object index') ]); }); }); diff --git a/src/parser/tests/expression/TemplateStringExpression.spec.ts b/src/parser/tests/expression/TemplateStringExpression.spec.ts index a38dd2ca4..e321a3b5c 100644 --- a/src/parser/tests/expression/TemplateStringExpression.spec.ts +++ b/src/parser/tests/expression/TemplateStringExpression.spec.ts @@ -90,7 +90,7 @@ describe('TemplateStringExpression', () => { it('catches missing closing backtick', () => { let { tokens } = Lexer.scan('name = `hello world'); let parser = Parser.parse(tokens, { mode: ParseMode.BrighterScript }); - expect(parser.diagnostics[0]?.message).to.equal(DiagnosticMessages.unterminatedTemplateStringAtEndOfFile().message); + expect(parser.diagnostics[0]?.message).to.equal(DiagnosticMessages.unterminatedTemplateString().message); }); }); diff --git a/src/parser/tests/statement/Dim.spec.ts b/src/parser/tests/statement/Dim.spec.ts index 3c0f7c804..a1eeb5936 100644 --- a/src/parser/tests/statement/Dim.spec.ts +++ b/src/parser/tests/statement/Dim.spec.ts @@ -2,6 +2,7 @@ import { expect } from '../../../chai-config.spec'; import type { DimStatement } from '../../Statement'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import { Parser } from '../../Parser'; +import { expectDiagnostics } from '../../../testHelpers.spec'; describe('parser DimStatement', () => { it('parses properly', () => { @@ -31,7 +32,7 @@ describe('parser DimStatement', () => { //the statement should still exist and have null identifier expect(dimStatement).to.exist; expect(dimStatement.tokens.name).to.not.exist; - expect(parser.diagnostics.map(x => x.message)).to.include(DiagnosticMessages.expectedIdentifierAfterKeyword('dim').message); + expect(parser.diagnostics.map(x => x.message)).to.include(DiagnosticMessages.expectedIdentifier('dim').message); }); it('flags missing left bracket', () => { @@ -40,7 +41,11 @@ describe('parser DimStatement', () => { //the statement should still exist and have null dimensions expect(dimStatement).to.exist; expect(dimStatement.tokens.openingSquare).to.not.exist; - expect(parser.diagnostics.map(x => x.message)).to.include(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier().message); + expectDiagnostics(parser, [ + DiagnosticMessages.missingExpressionsInDimStatement().message, + DiagnosticMessages.expectedToken('[').message, + DiagnosticMessages.unexpectedToken(']').message + ]); }); it('flags missing right bracket', () => { @@ -49,7 +54,9 @@ describe('parser DimStatement', () => { //the statement should still exist and have null dimensions expect(dimStatement).to.exist; expect(dimStatement.tokens.closingSquare).to.not.exist; - expect(parser.diagnostics.map(x => x.message)).to.include(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier().message); + expectDiagnostics(parser, [ + DiagnosticMessages.unmatchedLeftToken('[', 'dim identifier').message + ]); }); it('flags missing expression(s)', () => { diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts index b6e458ea0..861ccd16f 100644 --- a/src/parser/tests/statement/Enum.spec.ts +++ b/src/parser/tests/statement/Enum.spec.ts @@ -439,10 +439,10 @@ describe('EnumStatement', () => { `); program.validate(); expectDiagnostics(program, [{ - ...DiagnosticMessages.unknownEnumValue('DOWN', 'Direction'), + ...DiagnosticMessages.cannotFindName('DOWN', 'Direction.DOWN', 'Direction', 'enum'), location: { range: util.createRange(7, 36, 7, 40) } }, { - ...DiagnosticMessages.unknownEnumValue('down', 'Direction'), + ...DiagnosticMessages.cannotFindName('down', 'Direction.down', 'Direction', 'enum'), location: { range: util.createRange(8, 36, 8, 40) } }]); }); @@ -463,10 +463,10 @@ describe('EnumStatement', () => { `); program.validate(); expectDiagnostics(program, [{ - ...DiagnosticMessages.unknownEnumValue('DOWN', 'Enums.Direction'), + ...DiagnosticMessages.cannotFindName('DOWN', 'Enums.Direction.DOWN', 'Enums.Direction', 'enum'), location: { range: util.createRange(2, 42, 2, 46) } }, { - ...DiagnosticMessages.unknownEnumValue('down', 'Enums.Direction'), + ...DiagnosticMessages.cannotFindName('down', 'Enums.Direction.down', 'Enums.Direction', 'enum'), location: { range: util.createRange(3, 42, 3, 46) } }]); }); diff --git a/src/parser/tests/statement/Increment.spec.ts b/src/parser/tests/statement/Increment.spec.ts index 7dae7df1e..6ff119e19 100644 --- a/src/parser/tests/statement/Increment.spec.ts +++ b/src/parser/tests/statement/Increment.spec.ts @@ -6,6 +6,7 @@ import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import util from '../../../util'; +import { expectDiagnostics } from '../../../testHelpers.spec'; describe('parser postfix unary expressions', () => { it('parses postfix \'++\' for variables', () => { @@ -56,11 +57,7 @@ describe('parser postfix unary expressions', () => { token(TokenKind.PlusPlus, '++'), EOF ]); - - expect(diagnostics).to.be.lengthOf(1); - expect(diagnostics[0]).deep.include({ - message: 'Consecutive increment/decrement operators are not allowed' - }); + expectDiagnostics(diagnostics, [DiagnosticMessages.unexpectedOperator().code]); }); it('disallows postfix \'--\' for function call results', () => { @@ -74,7 +71,7 @@ describe('parser postfix unary expressions', () => { expect(diagnostics).to.be.lengthOf(1); expect(diagnostics[0]).to.deep.include({ - ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall() + ...DiagnosticMessages.unexpectedOperator() }); }); diff --git a/src/parser/tests/statement/TryCatch.spec.ts b/src/parser/tests/statement/TryCatch.spec.ts index 33a4eabad..168a8f780 100644 --- a/src/parser/tests/statement/TryCatch.spec.ts +++ b/src/parser/tests/statement/TryCatch.spec.ts @@ -159,7 +159,7 @@ describe('parser try/catch', () => { `); expectDiagnosticsIncludes(parser, [ DiagnosticMessages.expectedCatchBlockInTryCatch().message, - DiagnosticMessages.expectedEndTryToTerminateTryCatch().message + DiagnosticMessages.expectedTerminator('end try', 'try-catch', 'statement').message ]); }); }); diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 1d65d7280..175ab6f7b 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -1,7 +1,7 @@ import type { BsDiagnostic } from './interfaces'; import * as assert from 'assert'; import chalk from 'chalk'; -import type { CodeDescription, CompletionItem, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, integer, Location } from 'vscode-languageserver'; +import type { CodeDescription, CompletionItem, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location } from 'vscode-languageserver'; import { createSandbox } from 'sinon'; import { expect } from './chai-config.spec'; import type { CodeActionShorthand } from './CodeActionUtil'; @@ -62,7 +62,7 @@ function cloneObject(original: TOriginal, template: TTempl interface PartialDiagnostic { location?: Partial; severity?: DiagnosticSeverity; - code?: integer | string; + code?: number | string; codeDescription?: Partial; source?: string; message?: string; @@ -70,6 +70,7 @@ interface PartialDiagnostic { relatedInformation?: Partial[]; data?: unknown; file?: Partial; + legacyCode?: number | string; } /** @@ -79,7 +80,7 @@ function cloneDiagnostic(actualDiagnosticInput: BsDiagnostic, expectedDiagnostic const actualDiagnostic = cloneObject( actualDiagnosticInput, expectedDiagnostic, - ['message', 'code', 'severity', 'relatedInformation'] + ['message', 'code', 'severity', 'relatedInformation', 'legacyCode'] ); //clone Location if available if (expectedDiagnostic?.location) { @@ -114,9 +115,14 @@ export function expectDiagnostics(arg: DiagnosticCollection, expected: Array { let result = x; if (typeof x === 'string') { - result = { message: x }; + if (!x.includes(' ') && x.toLowerCase() === x) { + // it's all lowercase and there's no spaces, so probably a code + result = { code: x }; + } else { + result = { message: x }; + } } else if (typeof x === 'number') { - result = { code: x }; + result = { legacyCode: x }; } return result as unknown as BsDiagnostic; }) @@ -128,7 +134,8 @@ export function expectDiagnostics(arg: DiagnosticCollection, expected: Array { let result = x; if (typeof x === 'string') { - result = { message: x }; + if (!x.includes(' ') && x.toLowerCase() === x) { + // it's all lowercase and there's no spaces, so probably a code + result = { code: x }; + } else { + result = { message: x }; + } } else if (typeof x === 'number') { - result = { code: x }; + result = { legacyCode: x }; } return result as unknown as BsDiagnostic; }); diff --git a/src/util.ts b/src/util.ts index 55b3f5b8d..a871e8659 100644 --- a/src/util.ts +++ b/src/util.ts @@ -205,7 +205,7 @@ export class Util { if (parseErrors.length > 0) { let err = parseErrors[0]; let diagnostic = { - ...DiagnosticMessages.bsConfigJsonHasSyntaxErrors(printParseErrorCode(parseErrors[0].error)), + ...DiagnosticMessages.syntaxError(`Syntax errors in bsconfig.json: ${printParseErrorCode(parseErrors[0].error)}`), location: { uri: this.pathToUri(configFilePath), range: this.getRangeFromOffsetLength(projectFileContents, err.offset, err.length) @@ -1871,7 +1871,7 @@ export class Util { return clone; //filter out null relatedInformation items }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)), - code: diagnostic.code, + code: diagnostic.code ? diagnostic.code : (diagnostic as BsDiagnostic).legacyCode, source: 'brs' } as Diagnostic; if (diagnostic?.tags?.length > 0) { diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts index 23252f007..6f5ca63c0 100644 --- a/src/validators/ClassValidator.ts +++ b/src/validators/ClassValidator.ts @@ -141,11 +141,20 @@ export class BsClassValidator { //mismatched member type (field/method in child, opposite in ancestor) if (memberType !== ancestorMemberKind) { + const childFieldType = member.getType({ flags: SymbolTypeFlag.typetime }); + let ancestorMemberType: BscType = new DynamicType(); + if (isFieldStatement(ancestorAndMember.member)) { + ancestorMemberType = ancestorAndMember.member.getType({ flags: SymbolTypeFlag.typetime }); + } else if (isMethodStatement(ancestorAndMember.member)) { + ancestorMemberType = ancestorAndMember.member.func.getType({ flags: SymbolTypeFlag.typetime }); + } this.diagnostics.push({ - ...DiagnosticMessages.classChildMemberDifferentMemberTypeThanAncestor( - memberType, - ancestorMemberKind, - ancestorAndMember.classStatement.getName(ParseMode.BrighterScript) + ...DiagnosticMessages.childFieldTypeNotAssignableToBaseProperty( + classStatement.getName(ParseMode.BrighterScript) ?? '', + ancestorAndMember.classStatement.getName(ParseMode.BrighterScript), + memberName.text, + childFieldType.toString(), + ancestorMemberType.toString() ), location: member.location });