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
154 changes: 154 additions & 0 deletions src/files/BrsFile.Class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2151,4 +2151,158 @@ describe('BrsFile BrighterScript classes', () => {
DiagnosticMessages.mismatchArgumentCount(1, 0)
]);
});

describe('conditional compile', () => {
it('transpiles methods inside conditional compile blocks', async () => {
await testTranspile(`
#const DEBUG = true
class Animal
sub speak()
print "speak"
end sub
#if DEBUG
sub debugSpeak()
print "debug"
end sub
#end if
end class
`, `
#const DEBUG = true
sub __Animal_method_new()
end sub
sub __Animal_method_speak()
print "speak"
end sub
#if DEBUG
sub __Animal_method_debugSpeak()
print "debug"
end sub
#end if
function __Animal_builder()
instance = {}
instance.new = __Animal_method_new
instance.speak = __Animal_method_speak
#if DEBUG
instance.debugSpeak = __Animal_method_debugSpeak
#end if
return instance
end function
function Animal()
instance = __Animal_builder()
instance.new()
return instance
end function
`, 'trim', 'source/main.bs');
});

it('transpiles fields inside conditional compile blocks as conditional initializers', async () => {
await testTranspile(`
#const DEBUG = true
class Animal
name = "generic"
#if DEBUG
logLevel = 4
#else
logLevel = 0
#end if
end class
`, `
#const DEBUG = true
sub __Animal_method_new()
m.name = "generic"
#if DEBUG
m.logLevel = 4
#else
m.logLevel = 0
#end if
end sub
function __Animal_builder()
instance = {}
instance.new = __Animal_method_new
return instance
end function
function Animal()
instance = __Animal_builder()
instance.new()
return instance
end function
`, 'trim', 'source/main.bs');
});

it('transpiles #else if chains of methods', async () => {
await testTranspile(`
#const DEBUG = true
#const BETA = false
class Animal
#if DEBUG
sub speak()
print "debug"
end sub
#else if BETA
sub speak()
print "beta"
end sub
#else
sub speak()
print "prod"
end sub
#end if
end class
`, `
#const DEBUG = true
#const BETA = false
sub __Animal_method_new()
end sub
#if DEBUG
sub __Animal_method_speak()
print "debug"
end sub
#else if BETA
sub __Animal_method_speak()
print "beta"
end sub
#else
sub __Animal_method_speak()
print "prod"
end sub
#end if
function __Animal_builder()
instance = {}
instance.new = __Animal_method_new
#if DEBUG
instance.speak = __Animal_method_speak
#else if BETA
instance.speak = __Animal_method_speak
#else
instance.speak = __Animal_method_speak
#end if
return instance
end function
function Animal()
instance = __Animal_builder()
instance.new()
return instance
end function
`, 'trim', 'source/main.bs');
});

it('does not produce diagnostics for conditional members used within the class', () => {
program.setFile('source/main.bs', `
#const DEBUG = true
class Animal
#if DEBUG
logLevel = 4
sub debugSpeak()
print m.logLevel
end sub
#end if
sub speak()
m.debugSpeak()
end sub
end class
`);
program.validate();
expectZeroDiagnostics(program);
});
});
});
152 changes: 150 additions & 2 deletions src/parser/Parser.Class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { DiagnosticMessages } from '../DiagnosticMessages';
import { TokenKind, AllowedLocalIdentifiers, AllowedProperties } from '../lexer/TokenKind';
import { Lexer } from '../lexer/Lexer';
import { Parser, ParseMode } from './Parser';
import type { FunctionStatement, AssignmentStatement, FieldStatement } from './Statement';
import type { FunctionStatement, AssignmentStatement, FieldStatement, ConditionalCompileStatement, MethodStatement, Block } from './Statement';
import { ClassStatement } from './Statement';
import { NewExpression } from './Expression';
import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics } from '../testHelpers.spec';
import { isClassStatement } from '../astUtils/reflection';
import { isClassStatement, isConditionalCompileStatement, isMethodStatement } from '../astUtils/reflection';
import { StringType } from '../types/StringType';
import { SymbolTypeFlag } from '../SymbolTypeFlag';
import util from '../util';
Expand Down Expand Up @@ -561,4 +561,152 @@ describe('parser class', () => {
klassMembers.forEach(sym => expect(sym.flags & SymbolTypeFlag.optional).to.eq(SymbolTypeFlag.optional));
});
});

describe('conditional compile', () => {
it('allows methods inside conditional compile blocks', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
sub speak()
end sub

#if DEBUG
sub debugSpeak()
print "debug"
end sub
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
expect(isClassStatement(klass)).to.be.true;
expect(isConditionalCompileStatement(klass.body[1])).to.be.true;
const cc = klass.body[1] as ConditionalCompileStatement;
expect(isMethodStatement(cc.thenBranch.statements[0])).to.be.true;
//both methods are registered on the class
expect(klass.methods.map(x => x.tokens.name.text)).to.eql(['speak', 'debugSpeak']);
expect(klass.memberMap['debugspeak']).to.exist;
});

it('allows fields inside conditional compile blocks', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
name as string
#if DEBUG
debugName as string
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
expect(klass.fields.map(x => x.tokens.name.text)).to.eql(['name', 'debugName']);
});

it('allows #else and #else if branches with members', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
#if DEBUG
sub speak()
print "debug"
end sub
#else if BETA
sub speak()
print "beta"
end sub
#else
sub speak()
print "prod"
end sub
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
const cc = klass.body[0] as ConditionalCompileStatement;
expect(isConditionalCompileStatement(cc)).to.be.true;
expect(isMethodStatement(cc.thenBranch.statements[0])).to.be.true;
const elseIf = cc.elseBranch as ConditionalCompileStatement;
expect(isConditionalCompileStatement(elseIf)).to.be.true;
expect(elseIf.tokens.condition.text).to.eq('BETA');
expect(isMethodStatement(elseIf.thenBranch.statements[0])).to.be.true;
expect(isMethodStatement((elseIf.elseBranch as Block).statements[0])).to.be.true;
//all three speak() methods are registered
expect(klass.methods).to.be.lengthOf(3);
});

it('allows nested conditional compile blocks', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
#if DEBUG
#if BETA
sub speak()
end sub
#end if
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
const outer = klass.body[0] as ConditionalCompileStatement;
expect(isConditionalCompileStatement(outer)).to.be.true;
const inner = outer.thenBranch.statements[0] as ConditionalCompileStatement;
expect(isConditionalCompileStatement(inner)).to.be.true;
expect(isMethodStatement(inner.thenBranch.statements[0])).to.be.true;
expect(klass.methods).to.be.lengthOf(1);
});

it('allows empty conditional compile blocks', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
#if DEBUG
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
expect(isConditionalCompileStatement(klass.body[0])).to.be.true;
});

it('allows annotations on members inside conditional compile blocks', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
#if DEBUG
@it("does something")
sub speak()
end sub
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
const cc = klass.body[0] as ConditionalCompileStatement;
const method = cc.thenBranch.statements[0] as MethodStatement;
expect(method.annotations?.[0]?.name).to.eq('it');
});

it('includes conditional members in the class type', () => {
let { ast, diagnostics } = Parser.parse(`
class Person
#if DEBUG
sub speak()
end sub
#end if
end class
`, { mode: ParseMode.BrighterScript });
expectZeroDiagnostics(diagnostics);
const klass = ast.statements[0] as ClassStatement;
const klassType = klass.getType({ flags: SymbolTypeFlag.typetime });
expect(klassType.getMemberTable().getSymbol('speak', SymbolTypeFlag.runtime)).to.exist;
});

it('flags unterminated conditional compile blocks in class bodies', () => {
let { diagnostics } = Parser.parse(`
class Person
#if DEBUG
sub speak()
end sub
end class
`, { mode: ParseMode.BrighterScript });
expect(diagnostics.length).to.be.greaterThan(0);
});
});
});
Loading
Loading