Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Additional v1 Syntax: #1059

Merged
merged 1 commit into from
Feb 7, 2024
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
53 changes: 53 additions & 0 deletions src/files/BrsFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3860,6 +3860,19 @@ describe('BrsFile', () => {
`);
});

it('allows built-in types for interface members', () => {
program.setFile<BrsFile>('source/main.bs', `
interface MyBase
regex as roRegex
node as roSGNodeLabel
sub outputMatches(textInput as string)
function getLabelParent() as roSGNode
end interface
`);
program.validate();
expectZeroDiagnostics(program);
});

it('allows extends on interfaces', () => {
testTranspile(`
interface MyBase
Expand All @@ -3886,5 +3899,45 @@ describe('BrsFile', () => {
program.validate();
expectZeroDiagnostics(program);
});

it('allows built-in types for class members', () => {
program.setFile<BrsFile>('source/main.bs', `
class MyBase
regex as roRegex
node as roSGNodeLabel

sub outputMatches(textInput as string)
matches = m.regex.match(textInput)
if matches.count() > 1
m.node.text = matches[1]
else
m.node.text = "no match"
end if
end sub

function getLabelParent() as roSGNode
return m.node.getParent()
end function
end class
`);
program.validate();
expectZeroDiagnostics(program);
});

it('allows types on lhs of assignments', () => {
testTranspile(`
sub foo(node as roSGNode)
nodeParent as roSGNode = node.getParent()
text as string = nodeParent.id
print text
end sub
`, `
sub foo(node as object)
nodeParent = node.getParent()
text = nodeParent.id
print text
end sub
`);
});
});
});
63 changes: 51 additions & 12 deletions src/parser/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,14 @@ export class Parser {
range: name.range
});
}
if (this.check(TokenKind.As)) {
// v1 syntax allows type declaration on lhs of assignment
this.warnIfNotBrighterScriptMode('typed assignment');

this.advance(); // skip 'as'
this.typeToken(); // skip typeToken;
}

let operator = this.consume(
DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
...AssignmentOperators
Expand Down Expand Up @@ -1170,10 +1178,33 @@ export class Parser {
// `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
// out what to do with it.
if (
this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers) &&
this.checkAnyNext(...AssignmentOperators)
this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
) {
return this.assignment();
if (this.checkAnyNext(...AssignmentOperators)) {
return this.assignment();
} else if (this.checkNext(TokenKind.As)) {
// may be a typed assignment - this is v1 syntax
const backtrack = this.current;
let validTypeExpression = false;
try {
// skip the identifier, and check for valid type expression
this.advance();
// skip the 'as'
this.advance();
// check if there is a valid type
const typeToken = this.typeToken(true);
const allowedNameKinds = [TokenKind.Identifier, ...DeclarableTypes, ...this.allowedLocalIdentifiers];
validTypeExpression = allowedNameKinds.includes(typeToken.kind);
} catch (e) {
// ignore any errors
} finally {
this.current = backtrack;
}
if (validTypeExpression) {
// there is a valid 'as' and type expression
return this.assignment();
}
}
}

//some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
Expand Down Expand Up @@ -1388,13 +1419,21 @@ export class Parser {
/**
* Get an expression with identifiers separated by periods. Useful for namespaces and class extends
*/
private getNamespacedVariableNameExpression() {
let firstIdentifier = this.consume(
DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
TokenKind.Identifier,
...this.allowedLocalIdentifiers
) as Identifier;

private getNamespacedVariableNameExpression(ignoreDiagnostics = false) {
let firstIdentifier: Identifier;
if (ignoreDiagnostics) {
if (this.checkAny(...this.allowedLocalIdentifiers)) {
firstIdentifier = this.advance() as Identifier;
} else {
throw new Error();
}
} else {
firstIdentifier = this.consume(
DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
TokenKind.Identifier,
...this.allowedLocalIdentifiers
) as Identifier;
}
let expr: DottedGetExpression | VariableExpression;

if (firstIdentifier) {
Expand Down Expand Up @@ -2618,7 +2657,7 @@ export class Parser {
* Will return a token of whatever is next to be parsed
* Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way
*/
private typeToken(): Token {
private typeToken(ignoreDiagnostics = false): Token {
let typeToken: Token;
let lookForUnions = true;
let isAUnion = false;
Expand All @@ -2632,7 +2671,7 @@ export class Parser {
} else if (this.options.mode === ParseMode.BrighterScript) {
try {
// see if we can get a namespaced identifer
const qualifiedType = this.getNamespacedVariableNameExpression();
const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
} catch {
//could not get an identifier - just get whatever's next
Expand Down
3 changes: 2 additions & 1 deletion src/validators/ClassValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ export class BsClassValidator {
const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
const currentNamespaceName = namespace?.getName(ParseMode.BrighterScript);
//check if this custom type is in our class map
if (!this.getClassByName(lowerFieldTypeName, currentNamespaceName) && !this.scope.hasInterface(lowerFieldTypeName) && !this.scope.hasEnum(lowerFieldTypeName)) {
const isBuiltInType = util.isBuiltInType(lowerFieldTypeName);
if (!isBuiltInType && !this.getClassByName(lowerFieldTypeName, currentNamespaceName) && !this.scope.hasInterface(lowerFieldTypeName) && !this.scope.hasEnum(lowerFieldTypeName)) {
this.diagnostics.push({
...DiagnosticMessages.cannotFindType(fieldTypeName),
range: statement.type?.range ?? statement.range,
Expand Down