Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5a8bdfc
Reduce per-Token lexer allocation to cut GC pressure while editing (#…
chrisdp Sep 3, 2026
3c5eeb9
Enable @typescript-eslint/no-unsafe-argument
tacoSr Sep 4, 2026
7bf9656
Avoid emitting a duplicate sourceMappingURL comment (#1786)
TwitchBronBron Sep 4, 2026
359f4b5
Restrict #1785 to type-only changes, reduce lint suppressions
TwitchBronBron Sep 4, 2026
fbaaa70
Merge branch 'master' into enable-no-unsafe-argument
TwitchBronBron Sep 4, 2026
da18794
Infer node type from findAncestor type-guard matchers
TwitchBronBron Sep 4, 2026
47db0aa
Enable @typescript-eslint/no-unsafe-argument (#1785)
TwitchBronBron Sep 4, 2026
d4bf86d
Merge remote-tracking branch 'origin/master' into find-ancestor-typed
TwitchBronBron Sep 4, 2026
75d908e
Recognize regex literals after `${` and `,`
TwitchBronBron Sep 4, 2026
83b50cb
Infer node type from findAncestor type-guard matchers (#1787)
TwitchBronBron Sep 4, 2026
02810f1
Merge branch 'master' into fix-regex-after-template-expression
TwitchBronBron Sep 4, 2026
9cb993f
Recognize regex literals after `${` and `,` (#1789)
TwitchBronBron Sep 4, 2026
a51f833
Fix nested curly braces in template strings (#1539)
Copilot Sep 4, 2026
a5b859c
Fix duplicate and crashing "find all references" results (#1791)
TwitchBronBron Sep 8, 2026
fdd8906
Add generic go-to-definition for file path strings in BRS/BS/XML file…
Copilot Sep 8, 2026
97a0f40
Add `isTerminal` and `previousInChain` getters to AstNode (#1788)
TwitchBronBron Sep 8, 2026
24df692
Better error message for wrong-cased XML tags (#1793)
TwitchBronBron Sep 8, 2026
de232a0
Transpile continue down for firmware below 11.5 (#489)
TwitchBronBron Sep 8, 2026
e3f37b9
0.73.2 (#1794)
rokucommunity-bot[bot] Sep 8, 2026
a66b626
chore: Security enhancements (#1796)
TwitchBronBron Sep 9, 2026
afde8d1
Modifies default max worker thread logic to be only as much as memory…
markwpearce Sep 9, 2026
133b0ac
0.73.3 (#1799)
rokucommunity-bot[bot] Sep 9, 2026
7dd2478
Merge master into v1
TwitchBronBron Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ module.exports = {
'@typescript-eslint/no-parameter-properties': 'off',
//had to add this rule to prevent eslint from crashing
'@typescript-eslint/no-restricted-imports': ['off', {}],
//mitigating this sometimes results in undesirably verbose code. Should investigate enabling again in the future.
'@typescript-eslint/no-unsafe-argument': 'off',
//master enabled this as 'error' in #1785 after cleaning up the v0 call sites. v1's parser/validator
//rewrites introduced ~76 more violations that were never part of that cleanup, so it's a warning here
//until they're addressed. TODO raise back to 'error' once the remaining violations are fixed.
'@typescript-eslint/no-unsafe-argument': 'warn',
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': [
'error',
Expand Down
25 changes: 25 additions & 0 deletions docs/bsconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,33 @@ With this setting, using optional chaining (`?.`) without the version requiremen
| Feature | Minimum Version |
|---------|----------------|
| Optional chaining (`?.`, `?[`, `?(`) | 11.0.0 |
| `continue for` / `continue while` | 11.5.0 |
| Multi-line expressions / line continuation in `.brs` files | 15.3.0 |

### `continue` and older firmware

`continue for` and `continue while` were introduced in Roku OS 11.5, but unlike optional chaining they
*can* be transpiled down. When a file is transpiled and `minFirmwareVersion` is below `11.5.0`, each
`continue` is rewritten into a `goto` targeting a label at the end of the enclosing loop body:

```brightscript
' source
for i = 0 to 10
continue for
end for

' transpiled output when targeting below 11.5.0
for i = 0 to 10
goto BRIGHTERSCRIPT_CONTINUE_0
BRIGHTERSCRIPT_CONTINUE_0:
end for
```

Files that are **not** transpiled (a `.brs` file without
[`allowBrighterScriptInBrightScript`](#allowbrighterscriptinbrightscript)) are emitted as-is, so
there is nothing to rewrite. Those get an error instead, since the code would fail on the target
device.

### Line continuation in `.brs` files

In BrighterScript (`.bs`) files, multi-line expressions are supported because those constructs are transpiled away before reaching the device. In plain BrightScript (`.brs`) files, Roku OS 15.3 added native support for the same feature.
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"brace-expansion@^2": "^2.1.4",
"fast-uri": "^3.1.6",
"js-yaml@^3": "^3.15.1",
"js-yaml@^4": "^4.3.1",
"js-yaml@^4": "^4.3.2",
"nanoid": "^3.3.18",
"postcss": "^8.5.26",
"@babel/core": "^7.29.7"
Expand Down
3 changes: 3 additions & 0 deletions src/BusyStatusTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,11 @@ export class BusyStatusTracker<T = any> {
public once(eventName: 'change'): Promise<BusyStatus>;
public once<T>(eventName: string): Promise<T> {
return new Promise<T>((resolve) => {
//the widened `eventName` can't match the literal-string `on()` overload
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const off = this.on(eventName as any, (data) => {
off();
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
resolve(data as any);
});
});
Expand Down
22 changes: 16 additions & 6 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Position } from 'vscode-languageserver';
import { DiagnosticSeverity } from 'vscode-languageserver';
import type { BsDiagnostic, TypeCompatibilityData } from './interfaces';
import type { BsDiagnostic, DiagnosticCode, TypeCompatibilityData } from './interfaces';
import { TokenKind } from './lexer/TokenKind';
import util from './util';
import { SymbolTypeFlag } from './SymbolTypeFlag';
Expand Down Expand Up @@ -1191,6 +1191,11 @@ export let DiagnosticMessages = {
message: `Function name '${name}' is ${length} characters long, which exceeds the maximum of ${maxLength}. It will be truncated when converted with ToStr()`,
severity: DiagnosticSeverity.Warning,
code: 'function-name-too-long'
}),
xmlTagWrongCase: (actualTag: string, expectedTag: string) => ({
message: `Tag '${actualTag}' must be all lower case. Use '${expectedTag}' instead`,
severity: DiagnosticSeverity.Error,
code: 'xml-tag-wrong-case'
})
};
export const defaultMaximumTruncationLength = 160;
Expand Down Expand Up @@ -1294,12 +1299,17 @@ function formatAvailabilityAxis(axis: AvailabilityAxis, version: string): string

export const DiagnosticCodeMap = {} as Record<keyof (typeof DiagnosticMessages), string>;
export const DiagnosticLegacyCodeMap = {} as Record<keyof (typeof DiagnosticMessages), number>;
export let diagnosticCodes = [] as string[];
export let diagnosticCodes = [] as DiagnosticCode[];
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;
const typedKey = key as keyof typeof DiagnosticMessages;
//every factory returns an object with `code`/`legacyCode` properties regardless of its specific (possibly required)
//arguments, so it's safe to call each one with no arguments purely to read those codes off the result
const getCodes = DiagnosticMessages[typedKey] as () => { code: string; legacyCode?: number };
const codes = getCodes();
diagnosticCodes.push(codes.code);
diagnosticCodes.push(codes.legacyCode);
DiagnosticCodeMap[typedKey] = codes.code;
DiagnosticLegacyCodeMap[typedKey] = codes.legacyCode;
}

export interface DiagnosticInfo {
Expand Down
34 changes: 30 additions & 4 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,32 @@ describe('LanguageServer', () => {

expect(references).to.be.empty;
});

it('does not return duplicate locations for a file shared by multiple component scopes', async () => {
//a variable declared and used entirely within a file that many components include
const sharedDocument = addScriptFile('sharedLib', `
sub useShared()
sharedValue = 1
print sharedValue
end sub
`)!;
for (let i = 0; i < 3; i++) {
addXmlFile(`SharedHost${i}`, `<script type="text/brightscript" uri="sharedLib.brs" />`);
}

const references = await server['onReferences']({
textDocument: {
uri: sharedDocument.uri
},
position: util.createPosition(2, 22)
} as any);

//the assignment and the print usage, each reported exactly once even though the
//file is reachable through the source scope plus 3 component scopes
const keys = references.map(x => `${x.uri}:${x.range.start.line}:${x.range.start.character}`);
expect(keys).to.eql([...new Set(keys)]);
expect(references).to.be.lengthOf(2);
});
});

describe('onWillRenameFiles', () => {
Expand Down Expand Up @@ -1948,7 +1974,7 @@ describe('LanguageServer', () => {
});

expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(5);
expect(location.range.start.character).to.equal(16);
Expand All @@ -1963,7 +1989,7 @@ describe('LanguageServer', () => {
});

expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(5);
expect(location.range.start.character).to.equal(16);
Expand All @@ -1988,7 +2014,7 @@ describe('LanguageServer', () => {
position: util.createPosition(3, 36)
});
expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(referenceDocument.uri);
expect(location.range.start.line).to.equal(2);
expect(location.range.start.character).to.equal(20);
Expand Down Expand Up @@ -2023,7 +2049,7 @@ describe('LanguageServer', () => {
position: util.createPosition(3, 30)
});
expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(2);
expect(location.range.start.character).to.equal(20);
Expand Down
13 changes: 8 additions & 5 deletions src/LanguageServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as path from 'path';
import * as os from 'os';
import type {
CompletionItem,
Connection,
Expand Down Expand Up @@ -50,6 +49,7 @@ import { util } from './util';
import { DiagnosticCollection } from './DiagnosticCollection';
import { encodeSemanticTokens, semanticTokensLegend } from './SemanticTokenUtils';
import { LogLevel, createLogger, logger, setLspLoggerProps } from './logging';
import type { LogLevel as LogLevelText } from '@rokucommunity/logger';
import ignore from 'ignore';
import * as micromatch from 'micromatch';
import type { LspProject, LspDiagnostic } from './lsp/LspProject';
Expand All @@ -60,6 +60,7 @@ import * as fsExtra from 'fs-extra';
import type { FileChange, MaybePromise } from './interfaces';
import { Deferred } from './deferred';
import { workerPool } from './lsp/worker/WorkerThreadProject';
import { getDefaultMaxWorkerThreads } from './lsp/worker/WorkerPool';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import isEqual = require('lodash.isequal');

Expand All @@ -83,7 +84,7 @@ export class LanguageServer {
* per-workspace settings. Once this limit is reached, additional projects are spread evenly across the
* existing worker threads instead of each getting a dedicated one.
*/
public static maxWorkerThreadsDefault = Math.max(1, os.cpus().length);
public static maxWorkerThreadsDefault = getDefaultMaxWorkerThreads();

/**
* The language server protocol connection, used to send and receive all requests and responses
Expand Down Expand Up @@ -353,7 +354,7 @@ export class LanguageServer {
if (typeof value === 'string') {
value = value.toLowerCase();
}
const logLevelNumeric = this.logger.getLogLevelNumeric(value as any);
const logLevelNumeric = this.logger.getLogLevelNumeric(value as LogLevelText | LogLevel);

if (typeof logLevelNumeric === 'number') {
return logLevelNumeric;
Expand Down Expand Up @@ -417,6 +418,7 @@ export class LanguageServer {
concurrencyLimit = 1;
}
this.projectManager.projectActivationConcurrencyLimit = concurrencyLimit;
this.logger.info(`projectActivationConcurrencyLimit set to ${concurrencyLimit}`);
}

/**
Expand All @@ -441,6 +443,7 @@ export class LanguageServer {
maxWorkerThreads = 1;
}
workerPool.maxWorkers = maxWorkerThreads;
this.logger.info(`maxWorkerThreads set to ${maxWorkerThreads}`);
}

@AddStackToErrorMessage
Expand Down Expand Up @@ -620,7 +623,7 @@ export class LanguageServer {
* Extract project paths from settings' projects list, expanding the workspaceFolder variable if necessary
*/
private normalizeProjectPaths(workspaceFolder: string, projects: (string | BrightScriptProjectConfiguration)[]): BrightScriptProjectConfiguration[] | undefined {
return projects?.reduce((acc, project) => {
return projects?.reduce<BrightScriptProjectConfiguration[]>((acc, project) => {
if (typeof project === 'string') {
acc.push({ path: project });
} else if (typeof project.path === 'string') {
Expand Down Expand Up @@ -711,7 +714,7 @@ export class LanguageServer {

const srcPath = util.uriToPath(params.textDocument.uri);

const result = this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
const result = await this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
return result;
}

Expand Down
18 changes: 9 additions & 9 deletions src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { LogLevelNumeric as LogLevel } from '@rokucommunity/logger';
*/
export class Logger {

public static subscribe(callback) {
public static subscribe(callback: (...args: any[]) => void) {
this.emitter.on('log', callback);
return () => {
this.emitter.off('log', callback);
Expand Down Expand Up @@ -44,11 +44,11 @@ export class Logger {
return '[' + chalk.grey(formatTimestamp()) + ']';
}

private writeToLog(method: (...consoleArgs: any[]) => void, ...args: any[]) {
private writeToLog(method: (...consoleArgs: any[]) => void, ...args: unknown[]) {
if (this._logLevel === LogLevel.trace) {
method = console.trace;
}
let finalArgs: any[] = [];
let finalArgs: unknown[] = [];
//evaluate any functions to get their values.
//This allows more complicated values to only be evaluated if this log level is active
for (let arg of args) {
Expand All @@ -74,7 +74,7 @@ export class Logger {
/**
* Log an error message to the console
*/
error(...messages) {
error(...messages: unknown[]) {
if (this._logLevel >= LogLevel.error) {
this.writeToLog(console.error, ...messages);
}
Expand All @@ -83,7 +83,7 @@ export class Logger {
/**
* Log a warning message to the console
*/
warn(...messages) {
warn(...messages: unknown[]) {
if (this._logLevel >= LogLevel.warn) {
this.writeToLog(console.warn, ...messages);
}
Expand All @@ -92,15 +92,15 @@ export class Logger {
/**
* Log a standard log message to the console
*/
log(...messages) {
log(...messages: unknown[]) {
if (this._logLevel >= LogLevel.log) {
this.writeToLog(console.log, ...messages);
}
}
/**
* Log an info message to the console
*/
info(...messages) {
info(...messages: unknown[]) {
if (this._logLevel >= LogLevel.info) {
this.writeToLog(console.info, ...messages);
}
Expand All @@ -109,7 +109,7 @@ export class Logger {
/**
* Log a debug message to the console
*/
debug(...messages) {
debug(...messages: unknown[]) {
if (this._logLevel >= LogLevel.debug) {
this.writeToLog(console.debug, ...messages);
}
Expand All @@ -118,7 +118,7 @@ export class Logger {
/**
* Log a debug message to the console
*/
trace(...messages) {
trace(...messages: unknown[]) {
if (this._logLevel >= LogLevel.trace) {
this.writeToLog(console.trace, ...messages);
}
Expand Down
Loading
Loading