Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,11 @@ export let DiagnosticMessages = {
message: `Non-void ${functionType} must return a value`,
code: 1142,
severity: DiagnosticSeverity.Error
}),
unterminatedReplacementIdentifier: () => ({
message: `Unterminated replacement identifier`,
code: 1143,
severity: DiagnosticSeverity.Error
})
};

Expand Down
32 changes: 32 additions & 0 deletions src/lexer/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ export class Lexer {
if (this.peek() === '.') {
this.advance();
this.addToken(TokenKind.Callfunc);
} else if (this.peek() === '{') {
this.replacementIdentifier();
} else if (this.source.slice(this.current, this.current + 4).toLowerCase() === 'stop') {
this.advance();
this.advance();
this.advance();
this.advance();
this.addToken(TokenKind.AtStop);
} else {
this.addToken(TokenKind.At);
}
Expand Down Expand Up @@ -643,6 +651,30 @@ export class Lexer {
}
}

private replacementIdentifier() {
//consume the {
this.advance();
let depth = 1;
while (depth > 0 && !this.isAtEnd()) {
let c = this.peek();
if (c === '{') {
depth++;
} else if (c === '}') {
depth--;
}
this.advance();
}

if (depth > 0) {
this.diagnostics.push({
...DiagnosticMessages.unterminatedReplacementIdentifier(),
range: this.rangeOf()
});
} else {
this.addToken(TokenKind.ReplacementIdentifier);
}
}

private templateQuasiString() {
let value = this.source.slice(this.start, this.current);
if (value !== '`') { // if this is an empty string straight after an expression, then we'll accidentally consume the backtick
Expand Down
2 changes: 2 additions & 0 deletions src/lexer/TokenKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export enum TokenKind {
LongIntegerLiteral = 'LongIntegerLiteral',
EscapedCharCodeLiteral = 'EscapedCharCodeLiteral', //this is used to capture things like `\n`, `\r\n` in template strings
RegexLiteral = 'RegexLiteral',
ReplacementIdentifier = 'ReplacementIdentifier',

//types
Void = 'Void',
Expand Down Expand Up @@ -81,6 +82,7 @@ export enum TokenKind {
QuestionLeftSquare = 'QuestionLeftSquare', // ?[
QuestionLeftParen = 'QuestionLeftParen', // ?(
QuestionAt = 'QuestionAt', // ?@
AtStop = 'AtStop', // @stop

// conditional compilation
HashIf = 'HashIf', // #if
Expand Down
9 changes: 6 additions & 3 deletions src/parser/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ export class Parser {
return this.aliasStatement();
}

if (this.check(TokenKind.Stop)) {
if (this.check(TokenKind.Stop) || this.check(TokenKind.AtStop)) {
return this.stopStatement();
}

Expand Down Expand Up @@ -2659,11 +2659,11 @@ export class Parser {
...AllowedProperties
);

// force it into an identifier so the AST makes some sense
name.kind = TokenKind.Identifier;
if (!name) {
break;
}
// force it into an identifier so the AST makes some sense
name.kind = TokenKind.Identifier;
expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
//only allow a single `@` expression
break;
Expand Down Expand Up @@ -2801,6 +2801,9 @@ export class Parser {
case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
return new VariableExpression(this.previous() as Identifier);

case this.match(TokenKind.ReplacementIdentifier):
return new LiteralExpression(this.previous());

case this.match(TokenKind.LeftParen):
let left = this.previous();
let expr = this.expression();
Expand Down
68 changes: 68 additions & 0 deletions src/parser/tests/ReplacementIdentifier.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

import { expect } from '../../chai-config.spec';
import { Parser } from '../Parser';
import { Lexer } from '../../lexer/Lexer';
import { LiteralExpression } from '../Expression';
import { TokenKind } from '../../lexer/TokenKind';

describe('ReplacementIdentifier', () => {
it('supports resource replacement syntax', () => {
const parser = Parser.parse(`
sub main()
print @{chromaIcons.STOP}
end sub
`);
expect(parser.diagnostics).to.be.empty;
const printStmt = parser.ast.statements[0]['func'].body.statements[0];
expect(printStmt.expressions[0]).to.be.instanceof(LiteralExpression);
expect(printStmt.expressions[0].token.kind).to.equal(TokenKind.ReplacementIdentifier);
expect(printStmt.expressions[0].token.text).to.equal('@{chromaIcons.STOP}');
});

it('supports empty replacement identifier', () => {
const parser = Parser.parse(`
sub main()
print @{}
end sub
`);
expect(parser.diagnostics).to.be.empty;
const printStmt = parser.ast.statements[0]['func'].body.statements[0];
expect(printStmt.expressions[0].token.text).to.equal('@{}');
});

it('reports error for unterminated resource replacement', () => {
const { diagnostics } = Lexer.scan(`
sub main()
print @{chromaIcons.STOP
end sub
`);
expect(diagnostics).to.not.be.empty;
expect(diagnostics[0].message).to.equal('Unterminated replacement identifier');
});

it('does not crash on complex expressions', () => {
const parser = Parser.parse(`
sub main()
print "value: " + @{some.value}
end sub
`);
expect(parser.diagnostics).to.be.empty;
});

it('supports nested replacement identifiers', () => {
const parser = Parser.parse(`
sub main()
print @{Script @{consts.REGISTRY_SECTION_PREFIX} + @{consts.env.reg.section}}
print @{Script "<icon>" + @{icons.PRIVATE_BASELINE_ADJUSTED} + "</icon>"}
print @{Script @{ui.detailsStatusInfo.height} / 2}
print @{Script Max(@{ui.hub.posterHeight}, @{ui.hubs.rowLabelHeight})}
end sub
`);
expect(parser.diagnostics).to.be.empty;
const statements = parser.ast.statements[0]['func'].body.statements;
expect(statements[0].expressions[0].token.text).to.equal('@{Script @{consts.REGISTRY_SECTION_PREFIX} + @{consts.env.reg.section}}');
expect(statements[1].expressions[0].token.text).to.equal('@{Script "<icon>" + @{icons.PRIVATE_BASELINE_ADJUSTED} + "</icon>"}');
expect(statements[2].expressions[0].token.text).to.equal('@{Script @{ui.detailsStatusInfo.height} / 2}');
expect(statements[3].expressions[0].token.text).to.equal('@{Script Max(@{ui.hub.posterHeight}, @{ui.hubs.rowLabelHeight})}');
});
});
19 changes: 19 additions & 0 deletions src/parser/tests/statement/Stop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,23 @@ describe('stop statement', () => {
let { diagnostics } = Parser.parse(tokens);
expect(diagnostics.length).to.equal(0);
});
it('supports @stop', () => {
let { diagnostics } = Parser.parse([token(TokenKind.AtStop, '@stop'), EOF]);
expect(diagnostics[0]).to.be.undefined;
});

it('supports @STOP', () => {
let { diagnostics } = Parser.parse([token(TokenKind.AtStop, '@STOP'), EOF]);
expect(diagnostics[0]).to.be.undefined;
});

it('lexer recognizes @stop', () => {
let { tokens } = Lexer.scan('@stop');
expect(tokens[0].kind).to.equal(TokenKind.AtStop);
});

it('lexer recognizes @STOP', () => {
let { tokens } = Lexer.scan('@STOP');
expect(tokens[0].kind).to.equal(TokenKind.AtStop);
});
});
2 changes: 2 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,8 @@ export class Util {
return new DoubleType(token.text);
case TokenKind.DoubleLiteral:
return new DoubleType();
case TokenKind.ReplacementIdentifier:
return new DynamicType();
case TokenKind.Dynamic:
return new DynamicType(token.text);
case TokenKind.Float:
Expand Down
Loading