Skip to content

Commit 3e6bd21

Browse files
Backported v1 inline interface syntax (#1592)
Co-authored-by: Bronley Plumb <bronley@gmail.com>
1 parent bc07fd1 commit 3e6bd21

3 files changed

Lines changed: 223 additions & 5 deletions

File tree

src/files/BrsFile.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4420,6 +4420,24 @@ describe('BrsFile', () => {
44204420
end sub
44214421
`);
44224422
});
4423+
4424+
describe('inline interfaces', () => {
4425+
it('transpiles to "dynamic"', () => {
4426+
testTranspile(`
4427+
function foo(input as {name as string}) as {id as string}
4428+
output as {id as string} = {id: input.name}
4429+
return output
4430+
end function
4431+
`, `
4432+
function foo(input as dynamic) as dynamic
4433+
output = {
4434+
id: input.name
4435+
}
4436+
return output
4437+
end function
4438+
`);
4439+
});
4440+
});
44234441
});
44244442

44254443
it('allows up to 63 function params', () => {

src/parser/Parser.spec.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { PrintStatement, FunctionStatement, NamespaceStatement, ImportStatement
99
import { Range } from 'vscode-languageserver';
1010
import { DiagnosticMessages } from '../DiagnosticMessages';
1111
import { isAliasStatement, isBlock, isCommentStatement, isFunctionStatement, isIfStatement, isIndexedGetExpression, isTypecastStatement } from '../astUtils/reflection';
12-
import { expectDiagnostics, expectZeroDiagnostics } from '../testHelpers.spec';
12+
import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics } from '../testHelpers.spec';
1313
import { BrsTranspileState } from './BrsTranspileState';
1414
import { SourceNode } from 'source-map';
1515
import { BrsFile } from '../files/BrsFile';
@@ -1422,6 +1422,131 @@ describe('parser', () => {
14221422
expect(((ast.statements[0] as FunctionStatement).func.body.statements[0] as AssignmentStatement).name.text).to.eq('alias');
14231423
});
14241424
});
1425+
1426+
describe('inline interfaces', () => {
1427+
it('inline interface param types disallowed in brightscript mode', () => {
1428+
let { diagnostics } = parse(`
1429+
sub test(foo as {x as string})
1430+
print foo.x
1431+
end sub
1432+
`, ParseMode.BrightScript);
1433+
expectDiagnosticsIncludes(diagnostics, [
1434+
DiagnosticMessages.functionParameterTypeIsInvalid('foo', '{').message
1435+
]);
1436+
});
1437+
1438+
it('inline interface return types disallowed in brightscript mode', () => {
1439+
let { diagnostics } = parse(`
1440+
function test() as {x as string}
1441+
print {x: "hello"}
1442+
end function
1443+
`, ParseMode.BrightScript);
1444+
expectDiagnosticsIncludes(diagnostics, [
1445+
DiagnosticMessages.invalidFunctionReturnType('{').message
1446+
]);
1447+
});
1448+
1449+
it('inline interface as param type', () => {
1450+
let { ast, diagnostics } = parse(`
1451+
sub test(foo as {x as string})
1452+
print foo.x
1453+
end sub
1454+
`, ParseMode.BrighterScript);
1455+
expectZeroDiagnostics(diagnostics);
1456+
expect(ast.statements.length).to.eq(1);
1457+
});
1458+
1459+
it('inline interface as return type', () => {
1460+
let { ast, diagnostics } = parse(`
1461+
function test() as {x as string}
1462+
print {x: "hello"}
1463+
end function
1464+
`, ParseMode.BrighterScript);
1465+
expectZeroDiagnostics(diagnostics);
1466+
expect(ast.statements.length).to.eq(1);
1467+
});
1468+
1469+
it('parses a big inline interface as param type', () => {
1470+
let { ast, diagnostics } = parse(`
1471+
sub test(foo as {
1472+
x as string,
1473+
y as {a as integer}
1474+
z})
1475+
print foo.x + y.a.toStr()
1476+
end sub
1477+
`, ParseMode.BrighterScript);
1478+
expectZeroDiagnostics(diagnostics);
1479+
expect(ast.statements.length).to.eq(1);
1480+
});
1481+
1482+
it('allows optional members', () => {
1483+
let { ast, diagnostics } = parse(`
1484+
sub test(p as {x as string, optional y})
1485+
end sub
1486+
`, ParseMode.BrighterScript);
1487+
expectZeroDiagnostics(diagnostics);
1488+
expect(ast.statements.length).to.eq(1);
1489+
});
1490+
1491+
it('is allowed as typecast', () => {
1492+
let { diagnostics } = parse(`
1493+
sub test(p)
1494+
print (p as {name as string}).name
1495+
end sub
1496+
`, ParseMode.BrighterScript);
1497+
expectZeroDiagnostics(diagnostics);
1498+
});
1499+
1500+
it('is allowed as class and interface field', () => {
1501+
let { diagnostics } = parse(`
1502+
class Klass
1503+
x as {name as string}
1504+
end class
1505+
interface Iface
1506+
y as {age as integer}
1507+
end interface
1508+
`, ParseMode.BrighterScript);
1509+
expectZeroDiagnostics(diagnostics);
1510+
});
1511+
1512+
it('can have custom type as member type', () => {
1513+
let { diagnostics } = parse(`
1514+
interface IFace
1515+
name as string
1516+
end interface
1517+
function test(z as {foo as IFace})
1518+
return z.foo.name
1519+
end function
1520+
`, ParseMode.BrighterScript);
1521+
expectZeroDiagnostics(diagnostics);
1522+
});
1523+
1524+
it('can have per-member doc comment', () => {
1525+
let { diagnostics } = parse(`
1526+
interface IFace
1527+
inline as {
1528+
' comment 1
1529+
name as string
1530+
' comment 2
1531+
age as integer
1532+
}
1533+
end interface
1534+
function test(z as {foo as IFace})
1535+
return z.foo.inline.name
1536+
end function
1537+
`, ParseMode.BrighterScript);
1538+
expectZeroDiagnostics(diagnostics);
1539+
});
1540+
1541+
it('can have string literals as members', () => {
1542+
let { diagnostics } = parse(`
1543+
function test(z as {"this is a stringliteral" as string})
1544+
return z["this is a stringliteral"]
1545+
end function
1546+
`, ParseMode.BrighterScript);
1547+
expectZeroDiagnostics(diagnostics);
1548+
});
1549+
});
14251550
});
14261551

14271552
function parse(text: string, mode?: ParseMode) {

src/parser/Parser.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2767,9 +2767,14 @@ export class Parser {
27672767
typeToken = this.advance();
27682768
} else if (this.options.mode === ParseMode.BrighterScript) {
27692769
try {
2770-
// see if we can get a namespaced identifer
2771-
const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
2772-
typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
2770+
if (this.check(TokenKind.LeftCurlyBrace)) {
2771+
// could be an inline interface
2772+
typeToken = this.inlineInterface();
2773+
} else {
2774+
// see if we can get a namespaced identifer
2775+
const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
2776+
typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
2777+
}
27732778
} catch {
27742779
//could not get an identifier - just get whatever's next
27752780
typeToken = this.advance();
@@ -2780,7 +2785,7 @@ export class Parser {
27802785
}
27812786
resultToken = resultToken ?? typeToken;
27822787
if (resultToken && this.options.mode === ParseMode.BrighterScript) {
2783-
// check for brackets
2788+
// check for brackets for typed arrays
27842789
while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
27852790
const leftBracket = this.advance();
27862791
const rightBracket = this.advance();
@@ -2802,6 +2807,76 @@ export class Parser {
28022807
return resultToken;
28032808
}
28042809

2810+
private inlineInterface() {
2811+
const openToken = this.advance();
2812+
const memberTokens: Token[] = [];
2813+
memberTokens.push(openToken);
2814+
while (this.matchAny(TokenKind.Newline, TokenKind.Comment)) { }
2815+
while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
2816+
let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
2817+
if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
2818+
if (this.check(TokenKind.As)) {
2819+
if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
2820+
// as <EOL>
2821+
// `as` is the field name
2822+
} else if (this.checkNext(TokenKind.As)) {
2823+
// as as ____
2824+
// first `as` is the field name
2825+
} else if (optionalKeyword) {
2826+
// optional as ____
2827+
// optional is the field name, `as` starts type
2828+
// rewind current token
2829+
optionalKeyword = null;
2830+
this.current--;
2831+
}
2832+
}
2833+
} else {
2834+
// no name after `optional` ... optional is the name
2835+
// rewind current token
2836+
optionalKeyword = null;
2837+
this.current--;
2838+
}
2839+
if (optionalKeyword) {
2840+
memberTokens.push(optionalKeyword);
2841+
}
2842+
if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
2843+
this.diagnostics.push({
2844+
...DiagnosticMessages.unexpectedToken(this.peek().text),
2845+
range: this.peek().range
2846+
});
2847+
throw this.lastDiagnosticAsError();
2848+
}
2849+
if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
2850+
this.advance();
2851+
} else {
2852+
this.diagnostics.push({
2853+
...DiagnosticMessages.unexpectedToken(this.peek().text),
2854+
range: this.peek().range
2855+
});
2856+
throw this.lastDiagnosticAsError();
2857+
}
2858+
2859+
if (this.check(TokenKind.As)) {
2860+
memberTokens.push(this.advance()); // as
2861+
memberTokens.push(this.typeToken()); // type
2862+
}
2863+
while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) { }
2864+
}
2865+
if (!this.check(TokenKind.RightCurlyBrace)) {
2866+
this.diagnostics.push({
2867+
...DiagnosticMessages.unexpectedToken(this.peek().text),
2868+
range: this.peek().range
2869+
});
2870+
throw this.lastDiagnosticAsError();
2871+
}
2872+
const closeToken = this.advance();
2873+
memberTokens.push(closeToken);
2874+
2875+
const completeInlineInterfaceToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(...memberTokens));
2876+
2877+
return completeInlineInterfaceToken;
2878+
}
2879+
28052880
private primary(): Expression {
28062881
switch (true) {
28072882
case this.matchAny(

0 commit comments

Comments
 (0)