Skip to content

Commit

Permalink
transpile consts when used in complex expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Jul 13, 2022
1 parent dd49626 commit b72094c
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 57 deletions.
134 changes: 81 additions & 53 deletions src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { isLiteralExpression, isVariableExpression } from '../../astUtils/reflection';
import { Cache } from '../../Cache';
import { createToken } from '../../astUtils/creators';
import { isBrsFile, isDottedGetExpression, isLiteralExpression, isVariableExpression } from '../../astUtils/reflection';
import type { BrsFile } from '../../files/BrsFile';
import type { BeforeFileTranspileEvent } from '../../interfaces';
import { TokenKind } from '../../lexer/TokenKind';
import type { Expression } from '../../parser/Expression';
import { LiteralExpression } from '../../parser/Expression';
import { ParseMode } from '../../parser/Parser';
import type { Scope } from '../../Scope';
import util from '../../util';

export class BrsFilePreTranspileProcessor {
Expand All @@ -12,69 +16,93 @@ export class BrsFilePreTranspileProcessor {
}

public process() {
this.replaceEnumValues();
if (isBrsFile(this.event.file)) {
this.iterateExpressions();
}
}

private replaceEnumValues() {
const membersByEnum = new Cache<string, Map<string, string>>();
private iterateExpressions() {
const scope = this.event.program.getFirstScopeForFile(this.event.file);
for (let expression of this.event.file.parser.references.expressions) {
if (expression) {
this.processExpression(expression, scope);
}
}
}

const scope = this.event.file.program.getFirstScopeForFile(this.event.file);
/**
* Given a string optionally separated by dots, find an enum related to it.
* For example, all of these would return the enum: `SomeNamespace.SomeEnum.SomeMember`, SomeEnum.SomeMember, `SomeEnum`
*/
private getEnumInfo(name: string, containingNamespace: string, scope: Scope) {
//look for the enum directly
let result = scope.getEnumFileLink(name, containingNamespace);

//skip this logic if current scope has no enums and no consts
if ((scope?.getEnumMap()?.size ?? 0) === 0 && (scope?.getConstMap()?.size ?? 0) === 0) {
return;
if (result) {
return {
enum: result.item
};
}
for (const expression of this.event.file.parser.references.expressions) {
let parts: string[];
//constants with no owner (i.e. SOME_CONST)
if (isVariableExpression(expression)) {
parts = [expression.name.text.toLowerCase()];
//assume we've been given the enum.member syntax, so pop the member and try again
const parts = name.split('.');
const memberName = parts.pop();
result = scope.getEnumMap().get(parts.join('.'));
if (result) {
const value = result.item.getMemberValue(memberName);
return {
enum: result.item,
value: new LiteralExpression(createToken(
//just use float literal for now...it will transpile properly with any literal value
value.startsWith('"') ? TokenKind.StringLiteral : TokenKind.FloatLiteral,
value
))
};
}
}

/**
* direct enum member (i.e. Direction.up),
* namespaced enum member access (i.e. Name.Space.Direction.up),
* namespaced const access (i.e. Name.Space.SOME_CONST) or class consts (i.e. SomeClass.SOME_CONST),
*/
} else {
parts = util.getAllDottedGetParts(expression)?.map(x => x.text.toLowerCase());
}
if (parts) {
//get the name of the member
const memberName = parts.pop();
//get the name of the enum (including leading namespace if applicable)
const ownerName = parts.join('.');
let containingNamespace = this.event.file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
private processExpression(expression: Expression, scope: Scope) {
let containingNamespace = this.event.file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);

/**
* Enum member replacements
*/
const theEnum = scope.getEnumFileLink(ownerName, containingNamespace)?.item;
if (theEnum) {
const members = membersByEnum.getOrAdd(ownerName, () => theEnum.getMemberValueMap());
const value = members?.get(memberName);
this.event.editor.overrideTranspileResult(expression, value);
continue;
}
const parts = util.splitExpression(expression);

/**
* const replacements
*/
const fullName = ownerName ? `${ownerName}.${memberName}` : memberName.toLowerCase();
const processedNames: string[] = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
let entityName: string;
if (isVariableExpression(part) || isDottedGetExpression(part)) {
processedNames.push(part?.name?.text?.toLocaleLowerCase());
entityName = processedNames.join('.');
} else {
return;
}

const constStatement = scope.getConstFileLink(fullName, containingNamespace)?.item;
let value: Expression;

//if we found a const, override the transpile result
if (constStatement) {
this.event.editor.setProperty(expression, 'transpile', (state) => {
return isLiteralExpression(constStatement.value)
//transpile primitive value as-is
? constStatement.value.transpile(state)
//wrap non-primitive value in parens
: ['(', ...constStatement.value.transpile(state), ')'];
});
continue;
//did we find a const? transpile the value
let constStatement = scope.getConstFileLink(entityName, containingNamespace)?.item;
if (constStatement) {
value = constStatement.value;
} else {
//did we find an enum member? transpile that
let enumInfo = this.getEnumInfo(entityName, containingNamespace, scope);
if (enumInfo?.value) {
value = enumInfo.value;
}
}

if (value) {
//override the transpile for this item.
this.event.editor.setProperty(part, 'transpile', (state) => {
if (isLiteralExpression(value)) {
return value.transpile(state);
} else {
//wrap non-literals with parens to prevent on-device compile errors
return ['(', ...value.transpile(state), ')'];
}
});
//we are finished handling this expression
return;
}
}
}
}
8 changes: 6 additions & 2 deletions src/bscPlugin/validation/ScopeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,19 @@ export class ScopeValidator {

const entityName = processedNames.join('.');

//if this is an enum member, stop validating here to prevent errors further down the chain
if (scope.getEnumMemberMap().has(entityName)) {
break;
}

if (
!scope.getEnumMemberMap().has(entityName) &&
!scope.getEnumMap().has(entityName) &&
!scope.getClassMap().has(entityName) &&
!scope.getConstMap().has(entityName) &&
!scope.getCallableByName(entityName) &&
!scope.namespaceLookup.has(entityName)
) {
//if this looks like an enum member, provide a nicer error message
//if this looks like an enum, provide a nicer error message
const theEnum = this.getEnum(scope, entityName)?.item;
if (theEnum) {
this.addMultiScopeDiagnostic(event, {
Expand Down
30 changes: 30 additions & 0 deletions src/parser/tests/statement/ConstStatement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,36 @@ describe('ConstStatement', () => {
end sub
`);
});

it('supports property access on complex objects', () => {
testTranspile(`
const DEFAULTS = {
enabled: true
}
sub main()
print DEFAULTS.enabled
end sub
`, `
sub main()
print ({
enabled: true
}).enabled
end sub
`);
});

it('supports calling methods on consts', () => {
testTranspile(`
const API_KEY ="ABC"
sub main()
print API_KEY.toString()
end sub
`, `
sub main()
print "ABC".toString()
end sub
`);
});
});

describe('completions', () => {
Expand Down
17 changes: 16 additions & 1 deletion src/parser/tests/statement/Enum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,21 @@ describe('EnumStatement', () => {
end sub
`);
});
});

it('transpiles enum values when used in complex expressions', () => {
testTranspile(`
sub main()
print Direction.up.toStr()
end sub
enum Direction
up = "up"
down = "down"
end enum
`, `
sub main()
print "up".toStr()
end sub
`);
});
});
});
24 changes: 23 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { DottedGetExpression, Expression, VariableExpression } from './pars
import { Logger, LogLevel } from './Logger';
import type { Identifier, Locatable, Token } from './lexer/Token';
import { TokenKind } from './lexer/TokenKind';
import { isDottedGetExpression, isExpression, isNamespacedVariableNameExpression, isVariableExpression } from './astUtils/reflection';
import { isCallExpression, isCallfuncExpression, isDottedGetExpression, isExpression, isIndexedGetExpression, isNamespacedVariableNameExpression, isVariableExpression, isXmlAttributeGetExpression } from './astUtils/reflection';
import { WalkMode } from './astUtils/visitors';
import { CustomType } from './types/CustomType';
import { SourceNode } from 'source-map';
Expand Down Expand Up @@ -1343,6 +1343,28 @@ export class Util {
return parts.reverse();
}

/**
* Break an expression into each part.
*/
public splitExpression(expression: Expression) {
const parts: Expression[] = [expression];
let nextPart = expression;
while (nextPart) {
if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
nextPart = nextPart.obj;
} else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
nextPart = nextPart.callee;

} else if (isNamespacedVariableNameExpression(nextPart)) {
nextPart = nextPart.expression;
} else {
break;
}
parts.unshift(nextPart);
}
return parts;
}

/**
* Returns an integer if valid, or undefined. Eliminates checking for NaN
*/
Expand Down

0 comments on commit b72094c

Please sign in to comment.