Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
233 changes: 233 additions & 0 deletions docs/named-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Named Arguments

BrighterScript supports calling functions with named arguments, allowing you to pass arguments by parameter name instead of by position.

## Basic Usage

Use `paramName: value` syntax inside a function call:

```brighterscript
sub greet(name as string, excited as boolean)
if excited
print "Hello, " + name + "!!!"
else
print "Hello, " + name
end if
end sub

sub main()
greet(name: "Bob", excited: true)
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

```brightscript
sub greet(name as string, excited as boolean)
if excited
print "Hello, " + name + "!!!"
else
print "Hello, " + name
end if
end sub

sub main()
greet("Bob", true)
end sub
```

</details>

## Supported Call Sites

Named arguments are supported for regular functions, namespace functions, and class constructors:

```brighterscript
namespace MyNs
sub greet(name as string, excited as boolean)
end sub
end namespace

class Point
function new(x as integer, y as integer)
end function
end class

sub main()
MyNs.greet(name: "Bob", excited: true)
p = new Point(x: 1, y: 2)
end sub
```

## Out-of-Order Arguments

Named arguments must be passed in the same order as the function declaration. Calling a function with named arguments out of declaration order is a compile-time error:

```brighterscript
sub createUser(name as string, age as integer, admin as boolean)
end sub

sub main()
' error: Named argument 'name' is out of order
createUser(admin: false, name: "Alice", age: 30)
end sub
```

### Quick fix

In editors with BrighterScript language support, a `Reorder named arguments to match function declaration` quick fix is offered for this diagnostic. Applying it rewrites the call to match the function signature:

```brighterscript
sub createUser(name as string, age as integer, admin as boolean)
end sub

sub main()
createUser(name: "Alice", age: 30, admin: false)
end sub
```

## Mixed Positional and Named Arguments

Positional arguments can be combined with named arguments. Positional arguments must come first:

```brighterscript
sub greet(name as string, title as string, excited as boolean)
end sub

sub main()
greet("Bob", excited: true, title: "Mr")
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

```brightscript
sub greet(name as string, title as string, excited as boolean)
end sub

sub main()
greet("Bob", "Mr", true)
end sub
```

</details>

## Skipping Optional Parameters

Named arguments let you skip optional middle parameters. BrighterScript inserts the default value for any skipped parameter:

```brighterscript
sub greet(name as string, title = "Mr", excited = false)
end sub

sub main()
' Skip "title", pass "excited" by name
greet(name: "Bob", excited: true)
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

```brightscript
sub greet(name as string, title = "Mr", excited = false)
end sub

sub main()
' Skip "title", pass "excited" by name
greet("Bob", "Mr", true)
end sub
```

</details>

If a skipped optional parameter has no default value, `invalid` is inserted:

```brighterscript
sub greet(name as string, title = invalid, excited = false)
end sub

sub main()
greet(name: "Bob", excited: true)
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

```brightscript
sub greet(name as string, title = invalid, excited = false)
end sub

sub main()
greet("Bob", invalid, true)
end sub
```

</details>

## Validation

BrighterScript validates named argument calls and reports the following errors:

### Unknown argument name

```brighterscript
sub greet(name as string)
end sub

greet(nope: "Bob") ' error: Unknown named argument 'nope' for function 'greet'
```

### Duplicate named argument

```brighterscript
greet(name: "Bob", name: "Alice") ' error: Named argument 'name' was already provided
```

### Positional argument after named argument

```brighterscript
greet(name: "Bob", true) ' error: Positional arguments cannot follow named arguments
```

### Named arguments on an unknown function

Named arguments require the function definition to be visible so BrighterScript can verify parameter names at compile time:

```brighterscript
unknownFunc(param: true) ' error: Cannot use named arguments when calling 'unknownFunc' because the function definition cannot be found
```

### Cross-scope parameter conflict

When a `.bs` file is shared across multiple component scopes and the same function name resolves to different parameter signatures in those scopes, named arguments cannot be safely transpiled (the same call site would need different positional output per scope). BrighterScript reports this as an error:

```brighterscript
' shared.bs (included by both CompA.xml and CompB.xml)
sub callFoo()
' error: Named arguments for 'foo' are ambiguous: the function has different
' parameter signatures across component scopes that share this file.
foo(a: 2, b: 1)
end sub
```

```brighterscript
' helperA.bs (included by CompA.xml)
sub foo(a, b)
end sub
```

```brighterscript
' helperB.bs (included by CompB.xml)
sub foo(b, a)
end sub
```

To resolve, give the conflicting functions distinct names or align their parameter order across scopes.

## BrighterScript Only

Named argument syntax is a BrighterScript feature and is not valid in plain BrightScript (`.brs`) files.
9 changes: 9 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ enum RemoteButton
end enum
```

## [Named Arguments](named-arguments.md)
```brighterscript
sub greet(name as string, excited as boolean)
end sub

' pass args by name, in any order
greet(excited: true, name: "Bob")
```

## [Namespaces](namespaces.md)
```brighterscript
namespace util
Expand Down
35 changes: 35 additions & 0 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,41 @@ export let DiagnosticMessages = {
message: `'${featureName}' requires Roku firmware version ${minimumVersion} or higher (current target is ${configuredVersion})`,
code: 1146,
severity: DiagnosticSeverity.Error
}),
duplicateFunctionParameter: (paramName: string) => ({
message: `Duplicate parameter name '${paramName}'`,
code: 1147,
severity: DiagnosticSeverity.Warning
}),
unknownNamedArgument: (argName: string, funcName: string) => ({
message: `Unknown named argument '${argName}' for function '${funcName}'`,
code: 1148,
severity: DiagnosticSeverity.Error
}),
namedArgDuplicate: (argName: string) => ({
message: `Named argument '${argName}' was already provided`,
code: 1149,
severity: DiagnosticSeverity.Error
}),
positionalArgAfterNamedArg: () => ({
message: `Positional arguments cannot follow named arguments`,
code: 1150,
severity: DiagnosticSeverity.Error
}),
namedArgsNotAllowedForUnknownFunction: (funcName: string) => ({
message: `Cannot use named arguments when calling '${funcName}' because the function definition cannot be found`,
code: 1151,
severity: DiagnosticSeverity.Error
}),
namedArgsCrossScopeConflict: (funcName: string) => ({
message: `Named arguments for '${funcName}' are ambiguous: the function has different parameter signatures across component scopes that share this file. Named arguments cannot be safely transpiled.`,
code: 1152,
severity: DiagnosticSeverity.Error
}),
namedArgOutOfOrder: (argName: string) => ({
message: `Named argument '${argName}' is out of order`,
code: 1153,
severity: DiagnosticSeverity.Error
})
};

Expand Down
7 changes: 6 additions & 1 deletion src/Scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Cache } from './Cache';
import { URI } from 'vscode-uri';
import type { BrsFile } from './files/BrsFile';
import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
import { isBrsFile, isMethodStatement, isCustomType, isFunctionType, isXmlFile, isNamespaceStatement, isEnumMemberStatement } from './astUtils/reflection';
import { isBrsFile, isMethodStatement, isCustomType, isFunctionType, isXmlFile, isNamespaceStatement, isEnumMemberStatement, isNamedArgumentExpression } from './astUtils/reflection';
import { SymbolTable } from './SymbolTable';
import type { Statement } from './parser/AstNode';
import { LogLevel } from './logging';
Expand Down Expand Up @@ -947,6 +947,11 @@ export class Scope {
private diagnosticDetectFunctionCallsWithWrongParamCount(file: BscFile, callableContainersByLowerName: CallableContainerMap) {
//validate all function calls
for (let expCall of file?.functionCalls ?? []) {
// Skip calls that use named arguments — those are fully validated by validateNamedArgCalls()
if (expCall.args.some(a => isNamedArgumentExpression(a.expression))) {
continue;
}

let callableContainersWithThisName = callableContainersByLowerName.get(expCall.name.toLowerCase());

//use the first item from callablesByLowerName, because if there are more, that's a separate error
Expand Down
5 changes: 4 additions & 1 deletion src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement, TypecastStatement, AliasStatement, TypeStatement } from '../parser/Statement';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, AAIndexedMemberExpression, TypeCastExpression, TernaryExpression, NullCoalescingExpression } from '../parser/Expression';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, AAIndexedMemberExpression, TypeCastExpression, TernaryExpression, NullCoalescingExpression, NamedArgumentExpression } from '../parser/Expression';
import type { BrsFile } from '../files/BrsFile';
import type { XmlFile } from '../files/XmlFile';
import type { BscFile, File, TypedefProvider } from '../interfaces';
Expand Down Expand Up @@ -213,6 +213,9 @@ export function isBinaryExpression(element: AstNode | undefined): element is Bin
export function isCallExpression(element: AstNode | undefined): element is CallExpression {
return element?.constructor.name === 'CallExpression';
}
export function isNamedArgumentExpression(element: AstNode | undefined): element is NamedArgumentExpression {
return element?.constructor.name === 'NamedArgumentExpression';
}
export function isFunctionExpression(element: AstNode | undefined): element is FunctionExpression {
return element?.constructor.name === 'FunctionExpression';
}
Expand Down
35 changes: 35 additions & 0 deletions src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,41 @@ describe('CodeActionsProcessor', () => {
});
});

describe('named argument ordering', () => {
it('offers quick fix for out-of-order named arguments', () => {
const file = program.setFile('source/main.bs', `
sub greet(name as string, excited as boolean)
end sub

sub main()
greet(excited: true, name: "Bob")
end sub
`);

testGetCodeActions(file, util.createRange(5, 41, 5, 41), [
'Reorder named arguments to match function declaration'
]);
});

it('reorders mixed positional and named args to declaration order', () => {
const file = program.setFile('source/main.bs', `
sub greet(name as string, title = "Mr" as string, excited = false as boolean)
end sub

sub main()
greet("Bob", excited: true, title: "Dr")
end sub
`);

program.validate();
const actions = program.getCodeActions(file.srcPath, util.createRange(5, 50, 5, 50));
const action = actions.find(x => x.title === 'Reorder named arguments to match function declaration');
const edit = Object.values(action.edit.changes)[0][0];

expect(edit.newText).to.equal(`"Bob", title: "Dr", excited: true`);
});
});

it('suggests imports at very start and very end of diagnostic', () => {
program.setFile('source/first.bs', `
namespace alpha
Expand Down
Loading
Loading