Files
android-dev-ext/langserver/java/body-parser3.js

2721 lines
97 KiB
JavaScript

/**
* Method body parsing is entirely linear and relies upon type processing being completed so
* we can resolve packages, types, fields, methods, parameters and locals along the way.
*
* Each token also contains detailed state information used for completion suggestions.
*/
const { JavaType, CEIType, PrimitiveType, ArrayType, UnresolvedType, Field, Method, Parameter, Constructor, signatureToType } = require('java-mti');
const { SourceMethod, SourceConstructor } = require('./source-type');
const ResolvedImport = require('./parsetypes/resolved-import');
const ParseProblem = require('./parsetypes/parse-problem');
const { TextBlock, BlockRange } = require('./parsetypes/textblock');
/**
* @typedef {SourceMethod|SourceConstructor} SourceMC
*/
/**
* @param {string} source
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function parseBody(source, method, imports, typemap) {
const body = method._decl.body().blockArray();
if (!body || !body.simplified.startsWith('{')) {
return null;
}
const tokens = tokenize(source, body.start, body.length);
const tokenlist = new TokenList(tokens);
let block = null;
try {
statementBlock(tokenlist, [], method, imports, typemap);
} catch (err) {
addproblem(tokenlist, ParseProblem.Information(tokenlist.current, `Parse failed: ${err.message}`));
}
return {
block,
tokens,
problems: tokenlist.problems,
}
}
/**
*
* @param {TokenList} tokens
* @param {ParseProblem} problem
*/
function addproblem(tokens, problem) {
tokens.problems.push(problem);
}
/**
* @param {Local[]} locals
* @param {Local[]} new_locals
*/
function addLocals(tokens, locals, new_locals) {
for (let local of new_locals) {
if (locals.find(l => l.name === local.name)) {
addproblem(tokens, ParseProblem.Error(local.decltoken, `Redeclared variable: ${local.name}`));
}
locals.unshift(local);
}
}
class TokenList {
constructor(tokens) {
this.tokens = tokens;
this.idx = -1;
/** @type {Token} */
this.current = null;
this.inc();
/** @type {ParseProblem[]} */
this.problems = [];
}
inc() {
for (;;) {
this.current = this.tokens[this.idx += 1];
if (!this.current || this.current.kind !== 'wsc') {
return this.current;
}
}
}
/**
* Check if the current token matches the specified value and consumes it
* @param {string} value
*/
isValue(value) {
if (this.current.value === value) {
this.inc();
return true;
}
return false;
}
/**
* Check if the current token matches the specified value and consumes it or reports an error
* @param {string} value
*/
expectValue(value) {
if (this.isValue(value)) {
return true;
}
addproblem(this, ParseProblem.Error(this.current, `${value} expected`));
return false;
}
get previous() {
for (let idx = this.idx-1; idx >= 0 ; idx--) {
if (idx === 0 || this.tokens[idx].kind !== 'wsc') {
return this.tokens[idx];
}
}
}
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
* @returns {ResolvedIdent|Local[]|Statement}
*/
function statement(tokens, locals, method, imports, typemap) {
let s;
switch(tokens.current.kind) {
case 'statement-kw':
s = statementKeyword(tokens, locals, method, imports, typemap);
return s;
case 'modifier':
case 'ident':
case 'primitive-type':
s = expression_or_var_decl(tokens, locals, method, imports, typemap);
if (Array.isArray(s)) {
addLocals(tokens, locals, s);
}
semicolon(tokens);
return s;
case 'string-literal':
case 'char-literal':
case 'number-literal':
case 'boolean-literal':
case 'object-literal':
case 'inc-operator':
case 'plumin-operator':
case 'unary-operator':
case 'open-bracket':
case 'new-operator':
s = expression(tokens, locals, method, imports, typemap);
semicolon(tokens);
return s;
}
switch(tokens.current.value) {
case ';':
tokens.inc();
return new EmptyStatement();
case '{':
return statementBlock(tokens, locals, method, imports, typemap);
}
addproblem(tokens, ParseProblem.Error(tokens.current, `Statement expected`));
tokens.inc();
return new InvalidStatement();
}
class Statement {}
class EmptyStatement extends Statement {}
class SwitchStatement extends Statement {
/** @type {ResolvedIdent} */
test = null;
cases = [];
caseBlocks = [];
}
class Block extends Statement {
statements = [];
}
class TryStatement extends Statement {
block = null;
catches = [];
}
class IfStatement extends Statement {
test = null;
statement = null;
elseStatement = null;
}
class WhileStatement extends Statement {
test = null;
statement = null;
}
class BreakStatement extends Statement {}
class ContinueStatement extends Statement {}
class DoStatement extends Statement {
test = null;
block = null;
}
class ReturnStatement extends Statement {
expression = null;
}
class ThrowStatement extends Statement {
expression = null;
}
class InvalidStatement extends Statement {}
class ForStatement extends Statement {
/** @type {ResolvedIdent[] | Local[]} */
init = null;
/** @type {ResolvedIdent} */
test = null;
/** @type {ResolvedIdent[]} */
update = null;
/** @type {ResolvedIdent} */
iterable = null;
/** @type {Statement} */
statement = null;
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function statementBlock(tokens, locals, method, imports, typemap) {
const b = new Block();
tokens.expectValue('{');
const block_locals = locals.slice();
while (!tokens.isValue('}')) {
const s = statement(tokens, block_locals, method, imports, typemap);
if (s instanceof EmptyStatement) {
addproblem(tokens, ParseProblem.Hint(tokens.previous, `Redundant semicolon`));
}
b.statements.push(s);
}
return b;
}
/**
* @param {TokenList} tokens
*/
function semicolon(tokens) {
if (tokens.isValue(';')) {
return;
}
addproblem(tokens, ParseProblem.Error(tokens.previous, 'Missing operator or semicolon'));
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function statementKeyword(tokens, locals, method, imports, typemap) {
let s;
switch (tokens.current.value) {
case 'if':
tokens.inc();
s = new IfStatement();
s.test = bracketedTest(tokens, locals, method, imports, typemap);
s.statement = nonVarDeclStatement(tokens, locals, method, imports, typemap);
if (tokens.isValue('else')) {
s.elseStatement = nonVarDeclStatement(tokens, locals, method, imports, typemap);
}
break;
case 'while':
tokens.inc();
s = new WhileStatement();
s.test = bracketedTest(tokens, locals, method, imports, typemap);
s.statement = nonVarDeclStatement(tokens, locals, method, imports, typemap);
break;
case 'break':
tokens.inc();
s = new BreakStatement();
semicolon(tokens);
break;
case 'continue':
tokens.inc();
s = new ContinueStatement();
semicolon(tokens);
break;
case 'switch':
tokens.inc();
s = new SwitchStatement();
switchBlock(s, tokens, locals, method, imports, typemap);
break;
case 'do':
tokens.inc();
s = new DoStatement();
s.block = statementBlock(tokens, locals, method, imports, typemap);
tokens.expectValue('while');
s.test = bracketedTest(tokens, locals, method, imports, typemap);
semicolon(tokens);
break;
case 'try':
tokens.inc();
s = new TryStatement();
s.block = statementBlock(tokens, locals, method, imports, typemap);
catchFinallyBlocks(s, tokens, locals, method, imports, typemap);
break;
case 'return':
tokens.inc();
s = new ReturnStatement();
s.expression = isExpressionStart(tokens.current) ? expression(tokens, locals, method, imports, typemap) : null;
if (method instanceof SourceMethod)
checkReturnExpression(tokens, method, s.expression);
else if (method instanceof SourceConstructor) {
if (s.expression) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Constructors are not allowed to return values`));
}
}
semicolon(tokens);
break;
case 'throw':
tokens.inc();
s = new ThrowStatement();
if (!tokens.isValue(';')) {
s.expression = isExpressionStart(tokens.current) ? expression(tokens, locals, method, imports, typemap) : null;
checkThrowExpression(tokens, s.expression, typemap);
semicolon(tokens);
}
break;
case 'for':
tokens.inc();
s = new ForStatement();
forStatement(s, tokens, locals.slice(), method, imports, typemap);
break;
default:
s = new InvalidStatement();
addproblem(tokens, ParseProblem.Error(tokens.current, `Unexpected token: ${tokens.current.value}`));
tokens.inc();
break;
}
return s;
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function bracketedTest(tokens, locals, method, imports, typemap) {
tokens.expectValue('(');
const e = expression(tokens, locals, method, imports, typemap);
if (e.variables[0] && !isTypeAssignable(PrimitiveType.map.Z, e.variables[0].type)) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Boolean expression expected, but type '${e.variables[0].type.fullyDottedTypeName}' found`));
}
tokens.expectValue(')');
return e;
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function nonVarDeclStatement(tokens, locals, method, imports, typemap) {
const s = statement(tokens, locals, method, imports, typemap);
if (Array.isArray(s)) {
addproblem(tokens, ParseProblem.Error(tokens.previous, `Variable declarations are not permitted as a single conditional statement.`));
}
return s;
}
/**
* @param {ForStatement} s
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function forStatement(s, tokens, locals, method, imports, typemap) {
tokens.expectValue('(');
if (!tokens.isValue(';')) {
s.init = expression_list_or_var_decl(tokens, locals, method, imports, typemap);
// s.init is always an array, so we need to check the element type
if (s.init[0] instanceof Local) {
// @ts-ignore
addLocals(tokens, locals, s.init);
}
if (tokens.current.value === ':') {
enhancedFor(s, tokens, locals, method, imports, typemap);
return;
}
semicolon(tokens);
}
// for-condition
if (!tokens.isValue(';')) {
s.test = expression(tokens, locals, method, imports, typemap);
semicolon(tokens);
}
// for-updated
if (!tokens.isValue(')')) {
s.update = expressionList(tokens, locals, method, imports, typemap);
tokens.expectValue(')');
}
s.statement = nonVarDeclStatement(tokens, locals, method, imports, typemap);
}
/**
* @param {ForStatement} s
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function enhancedFor(s, tokens, locals, method, imports, typemap) {
const colon = tokens.current;
tokens.inc();
// enhanced for
const iter_var = s.init[0];
if (!(iter_var instanceof Local)) {
addproblem(tokens, ParseProblem.Error(tokens.previous, `For iterator must be a single variable declaration`));
}
s.iterable = expression(tokens, locals, method, imports, typemap);
const value = s.iterable.variables[0];
if (!value) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Expression expected`));
}
if (iter_var instanceof Local) {
let is_iterable = false, is_assignable = false;
if (value && value.type instanceof ArrayType) {
is_iterable = true; // all arrays are iterable
is_assignable = isTypeAssignable(iter_var.type, value.type.elementType);
} else if (value.type instanceof CEIType) {
const iterables = getTypeInheritanceList(value.type).filter(t => t.rawTypeSignature === 'Ljava/lang/Iterable;');
is_iterable = iterables.length > 0;
is_assignable = true; // todo - check the specialised versions of iterable to match the type against iter_var
}
if (!is_iterable) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Type '${value.type.fullyDottedTypeName}' is not an array or a java.lang.Iterable type`));
}
else if (!is_assignable) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Variable of type '${iter_var.type.fullyDottedTypeName}' is not compatible with iterable expression of type '${value.type.fullyDottedTypeName}'`));
}
}
tokens.expectValue(')');
s.statement = nonVarDeclStatement(tokens, locals, method, imports, typemap);
}
/**
* @param {TryStatement} s
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function catchFinallyBlocks(s, tokens, locals, method, imports, typemap) {
for (;;) {
if (tokens.isValue('finally')) {
if (s.catches.find(c => c instanceof Block)) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Multiple finally blocks are not permitted`));
}
s.catches.push(statementBlock(tokens, locals, method, imports, typemap));
continue;
}
if (tokens.isValue('catch')) {
const catchinfo = {
types: [],
name: null,
block: null,
}
tokens.expectValue('(');
const mods = [];
while (tokens.current.kind === 'modifier') {
mods.push(tokens.current);
tokens.inc();
}
let t = catchType(tokens, locals, method, imports, typemap);
if (t) catchinfo.types.push(t);
while (tokens.isValue('|')) {
let t = catchType(tokens, locals, method, imports, typemap);
if (t) catchinfo.types.push(t);
}
if (tokens.current.kind === 'ident') {
catchinfo.name = tokens.current;
tokens.inc();
} else {
addproblem(tokens, ParseProblem.Error(tokens.current, `Variable identifier expected`));
}
tokens.expectValue(')');
let exceptionVar;
if (catchinfo.types[0] && catchinfo.name) {
checkLocalModifiers(tokens, mods);
exceptionVar = new Local(mods, catchinfo.name.value, catchinfo.name, catchinfo.types[0]);
}
catchinfo.block = statementBlock(tokens, [...locals, exceptionVar], method, imports, typemap);
s.catches.push(catchinfo);
continue;
}
if (!s.catches.length) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Missing catch or finally block`));
}
const first_finally_idx = s.catches.findIndex(c => c instanceof Block);
if (first_finally_idx >= 0) {
if (s.catches.slice(first_finally_idx).find(c => !(c instanceof Block))) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Catch blocks must be declared before a finally block`));
}
}
return;
}
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function catchType(tokens, locals, method, imports, typemap) {
const t = qualifiedTerm(tokens, locals, method, imports, typemap);
if (t.types[0]) {
return t.types[0];
}
addproblem(tokens, ParseProblem.Error(tokens.current, `Missing or invalid type`));
return new UnresolvedType(t.source);
}
/**
* @param {SwitchStatement} s
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function switchBlock(s, tokens, locals, method, imports, typemap) {
tokens.expectValue('(');
s.test = expression(tokens, locals, method, imports, typemap);
let test_type = null;
if (s.test.variables[0]) {
// test must be int-compatible or be a string
test_type = s.test.variables[0].type;
if (!/^(Ljava\/lang\/String;|[BSIC])$/.test(test_type.typeSignature)) {
test_type = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Expression of type '${s.test.variables[0].type.fullyDottedTypeName}' is not compatible with int or java.lang.String`));
}
}
tokens.expectValue(')');
tokens.expectValue('{');
while (!tokens.isValue('}')) {
if (/^(case|default)$/.test(tokens.current.value)) {
caseBlock(s, test_type, tokens, locals, method, imports, typemap);
continue;
}
addproblem(tokens, ParseProblem.Error(tokens.current, 'case statement expected'));
break;
}
return s;
}
/**
* @param {SwitchStatement} s
* @param {JavaType} test_type
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function caseBlock(s, test_type, tokens, locals, method, imports, typemap) {
const case_start_idx = s.cases.length;
caseExpressionList(s.cases, test_type, tokens, locals, method, imports, typemap);
const statements = [];
for (;;) {
if (/^(case|default|\})$/.test(tokens.current.value)) {
break;
}
const s = statement(tokens, locals, method, imports, typemap);
statements.push(s);
}
s.caseBlocks.push({
cases: s.cases.slice(case_start_idx),
statements,
});
}
/**
* @param {(ResolvedIdent|boolean)[]} cases
* @param {JavaType} test_type
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function caseExpressionList(cases, test_type, tokens, locals, method, imports, typemap) {
let c = caseExpression(cases, test_type, tokens, locals, method, imports, typemap);
if (!c) {
return;
}
while (c) {
cases.push(c);
c = caseExpression(cases, test_type, tokens, locals, method, imports, typemap);
}
}
/**
* @param {(ResolvedIdent|boolean)[]} cases
* @param {JavaType} test_type
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function caseExpression(cases, test_type, tokens, locals, method, imports, typemap) {
/** @type {boolean|ResolvedIdent} */
let e = tokens.isValue('default');
if (e && cases.find(c => c === e)) {
addproblem(tokens, ParseProblem.Error(tokens.previous, `Duplicate case: default`))
}
if (!e) {
if (tokens.isValue('case')) {
e = expression(tokens, locals, method, imports, typemap);
if (e.variables[0]) {
if (test_type && !isAssignable(test_type, e.variables[0])) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Incompatible types: Expression of type '${e.variables[0].type.fullyDottedTypeName}' is not comparable to an expression of type '${test_type.fullyDottedTypeName}'`));
}
if (!isConstantValue(e.variables[0])) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Constant expression required`));
}
}
// todo - check duplicate non-default cases
}
}
if (e) {
tokens.expectValue(':');
}
return e;
}
/**
* @param {Local | Parameter | Field | ArrayElement | Value} v
*/
function isConstantValue(v) {
if (v instanceof Local) {
return !!v.finalToken;
}
if (v instanceof Field) {
return v.modifiers.includes('final');
}
if (v instanceof AnyValue) {
return true;
}
// Parameters and ArrayElements are never constant
return v instanceof LiteralValue;
}
/**
* @param {TokenList} tokens
* @param {Method} method
* @param {ResolvedIdent} return_expression
*/
function checkReturnExpression(tokens, method, return_expression) {
if (!return_expression && method.returnType.typeSignature === 'V') {
return;
}
if (return_expression && method.returnType.typeSignature === 'V') {
addproblem(tokens, ParseProblem.Error(tokens.current, `void methods cannot return values`));
return;
}
if (!return_expression && method.returnType.typeSignature !== 'V') {
addproblem(tokens, ParseProblem.Error(tokens.current, `Method must return a value of type '${method.returnType.fullyDottedTypeName}'`));
return;
}
const expr = return_expression.variables[0];
if (!expr) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Method must return a value of type '${method.returnType.fullyDottedTypeName}'`));
return;
}
const is_assignable = isAssignable(method.returnType, expr);
if (!is_assignable) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Incompatible types: Expression of type '${expr.type.fullyDottedTypeName}' cannot be returned from a method of type '${method.returnType.fullyDottedTypeName}'`));
}
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} throw_expression
* @param {Map<string,JavaType>} typemap
*/
function checkThrowExpression(tokens, throw_expression, typemap) {
if (!throw_expression.variables[0]) {
return;
}
let is_throwable = isAssignable(typemap.get('java/lang/Throwable'), throw_expression.variables[0]);
if (!is_throwable) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Incompatible types: throw expression must inherit from java.lang.Throwable`));
}
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
* @returns {ResolvedIdent|Local[]}
*/
function expression_or_var_decl(tokens, locals, method, imports, typemap) {
const mods = [];
while (tokens.current.kind === 'modifier') {
mods.push(tokens.current);
tokens.inc();
}
/** @type {ResolvedIdent} */
let matches = expression(tokens, locals, method, imports, typemap);
// if theres at least one type followed by an ident, we assume a variable declaration
if (matches.types[0] && tokens.current.kind === 'ident') {
const new_locals = [];
checkLocalModifiers(tokens, mods);
for (;;) {
let local = new Local(mods, tokens.current.value, tokens.current, matches.types[0]);
tokens.inc();
if (tokens.isValue('=')) {
const op = tokens.previous;
local.init = expression(tokens, locals, method, imports, typemap);
if (local.init.variables[0])
checkAssignmentExpression(tokens, local, op, local.init.variables[0]);
}
new_locals.push(local);
if (tokens.isValue(',')) {
if (tokens.current.kind === 'ident') {
continue;
}
addproblem(tokens, ParseProblem.Error(tokens.current, `Variable name expected`));
}
break;
}
return new_locals;
}
if (mods.length) {
addproblem(tokens, ParseProblem.Error(mods[0], `Unexpected token: '${mods[0].value}'`))
}
return matches;
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
* @returns {ResolvedIdent[]|Local[]}
*/
function expression_list_or_var_decl(tokens, locals, method, imports, typemap) {
let e = expression_or_var_decl(tokens, locals, method, imports, typemap);
if (Array.isArray(e)) {
// local var decl
return e;
}
const expressions = [e];
while (tokens.isValue(',')) {
e = expression(tokens, locals, method, imports, typemap);
expressions.push(e);
}
return expressions;
}
/**
* @param {Token[]} mods
*/
function checkLocalModifiers(tokens, mods) {
for (let i=0; i < mods.length; i++) {
if (mods[i].value !== 'final') {
addproblem(tokens, ParseProblem.Error(mods[i], `Modifier '${mods[i].source}' cannot be applied to local variable declarations.`));
} else if (mods.findIndex(m => m.source === 'final') < i) {
addproblem(tokens, ParseProblem.Error(mods[i], `Repeated 'final' modifier.`));
}
}
}
/**
* Operator precedence levels.
* Lower number = higher precedence.
* Operators with equal precedence are evaluated left-to-right.
*/
const operator_precedences = {
'*': 1, '%': 1, '/': 1,
'+': 2, '-': 2,
'<<': 3, '>>': 3, '>>>': 3,
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
'==': 5, '!=': 5,
'&': 6, '^': 7, '|': 8,
'&&': 9, '||': 10,
'?': 11,
'=': 12,
'+=':12,'-=':12,'*=':12,'/=':12,'%=':12,
'<<=':12,'>>=':12, '&=':12, '|=':12, '^=':12,
'&&=':12, '||=':12,
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function expression(tokens, locals, method, imports, typemap, precedence_stack = [13]) {
/** @type {ResolvedIdent} */
let matches = qualifiedTerm(tokens, locals, method, imports, typemap);
for(;;) {
if (!/^(assignment|equality|comparison|bitwise|shift|logical|muldiv|plumin|instanceof)-operator/.test(tokens.current.kind) && !/\?/.test(tokens.current.value)) {
break;
}
const binary_operator = tokens.current;
const operator_precedence = operator_precedences[binary_operator.source];
if (operator_precedence > precedence_stack[0]) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (operator_precedence === precedence_stack[0] && binary_operator.source !== '?' && binary_operator.kind !== 'assignment-operator') {
// equal precedence, ltr evaluation
break;
}
tokens.inc();
// higher or equal precendence with rtl evaluation
const rhs = expression(tokens, locals, method, imports, typemap, [operator_precedence, ...precedence_stack]);
if (binary_operator.value === '?') {
const colon = tokens.current;
tokens.expectValue(':');
const falseStatement = expression(tokens, locals, method, imports, typemap, [operator_precedence, ...precedence_stack]);
matches = resolveTernaryExpression(tokens, matches, colon, rhs, falseStatement);
} else {
matches = resolveBinaryOpExpression(tokens, matches, binary_operator, rhs);
}
}
return matches;
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} test
* @param {Token} colon
* @param {ResolvedIdent} truthy
* @param {ResolvedIdent} falsey
*/
function resolveTernaryExpression(tokens, test, colon, truthy, falsey) {
const ident = `${test.source} ? ${truthy.source} : ${falsey.source}`;
if (!truthy.variables[0] || !falsey.variables[0]) {
return new ResolvedIdent(ident);
}
return new ResolvedIdent(ident, [new TernaryValue(ident, truthy.variables[0].type, colon, falsey.variables[0])]);
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveBinaryOpExpression(tokens, lhs, op, rhs) {
const ident = `${lhs.source} ${op.value} ${rhs.source}`
switch(op.kind) {
case 'assignment-operator':
return resolveAssignment(tokens, ident, lhs, op, rhs);
case 'equality-operator':
return resolveEquality(tokens, ident, lhs, op, rhs);
case 'comparison-operator':
return resolveComparison(tokens, ident, lhs, op, rhs);
case 'bitwise-operator':
return resolveBitwise(tokens, ident, lhs, op, rhs);
case 'shift-operator':
return resolveShift(tokens, ident, lhs, op, rhs);
case 'logical-operator':
return resolveLogical(tokens, ident, lhs, op, rhs);
case 'instanceof-operator':
return resolveInstanceOf(tokens, ident, lhs, op, rhs);
case 'plumin-operator':
case 'muldiv-operator':
return resolveMath(tokens, ident, lhs, op, rhs);
}
throw new Error(`Unhandled binary operator: ${op.kind}`)
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveAssignment(tokens, ident, lhs, op, rhs) {
if (!lhs.variables[0] || !rhs.variables[0]) {
addproblem(tokens, ParseProblem.Error(op, `Invalid expression: ${ident}`));
return new ResolvedIdent(ident);
}
const lhsvar = lhs.variables[0];
let rhsvar = rhs.variables[0];
const pre_assign_operator = op.value.slice(0, -1);
if (pre_assign_operator) {
switch (getOperatorType(pre_assign_operator)) {
case "bitwise-operator":
// ^&| are both bitwise and logical operators
checkOperator(tokens, lhsvar, op, rhsvar, /^[BSIJCZ]{2}$/);
rhsvar = new Value(rhs.source, lhsvar.type);
break;
case "shift-operator":
checkOperator(tokens, lhsvar, op, rhsvar, /^[BSIJC]{2}$/);
rhsvar = new Value(rhs.source, lhsvar.type);
break;
case "logical-operator":
checkOperator(tokens, lhsvar, op, rhsvar, /^ZZ$/);
break;
case "muldiv-operator":
checkOperator(tokens, lhsvar, op, rhsvar, /^([BSIJC]{2}|[FD][BSIJCFD])$/);
rhsvar = new Value(rhs.source, lhsvar.type);
break;
case "plumin-operator":
if (pre_assign_operator === '+' && lhsvar.type.typeSignature === 'Ljava/lang/String;') {
// implicitly cast the rhs to a String value
rhsvar = new Value(rhs.source, lhsvar.type);
} else {
checkOperator(tokens, lhsvar, op, rhsvar, /^([BSIJC]{2}|[FD][BSIJCFD])$/);
rhsvar = new Value(rhs.source, lhsvar.type);
}
break;
}
}
checkAssignmentExpression(tokens, lhsvar, op, rhsvar);
// the result type is always the lhs
// e.g float = double = int will fail because of failure to convert from double to float
return new ResolvedIdent(lhsvar.name, [new Value(lhsvar.name, lhsvar.type)]);
}
/**
* @param {TokenList} tokens
* @param {Local|Parameter|Field|ArrayElement|Value} variable
* @param {Token} op
* @param {Local|Parameter|Field|ArrayElement|Value} value
*/
function checkAssignmentExpression(tokens, variable, op, value) {
if (variable instanceof AnyValue || value instanceof AnyValue) {
return true;
}
if (variable instanceof Value) {
addproblem(tokens, ParseProblem.Error(op, `Invalid assignment: left-hand side is not a variable`));
return;
}
let is_assignable;
// we need to special-case ArrayLiteral because it has no type associated with it
if (value instanceof ArrayLiteral) {
is_assignable = isArrayAssignable(variable.type, value);
if (!is_assignable) {
addproblem(tokens, ParseProblem.Error(op, `Array literal expression is not compatible with variable of type '${variable.type.fullyDottedTypeName}'`));
}
return;
}
is_assignable = isAssignable(variable.type, value);
if (!is_assignable) {
addproblem(tokens, ParseProblem.Error(op, `Incompatible types: Expression of type '${value.type.fullyDottedTypeName}' cannot be assigned to a variable of type '${variable.type.fullyDottedTypeName}'`));
}
if (value instanceof TernaryValue) {
checkAssignmentExpression(tokens, variable, value.colon, value.falseValue);
}
}
/**
* @param {JavaType} variable_type
* @param {ArrayLiteral} value
*/
function isArrayAssignable(variable_type, value) {
if (!(variable_type instanceof ArrayType)) {
return false;
}
// empty array literals are compatible with all arrays
if (value.elements.length === 0) {
return true;
}
const required_element_type = variable_type.arrdims > 1 ? new ArrayType(variable_type.base, variable_type.arrdims - 1) : variable_type.base;
for (let i=0; i < value.elements.length; i++) {
const element = value.elements[i];
let is_assignable;
if (required_element_type instanceof ArrayType) {
// the element must be another array literal expression or a value with a matching array type
if (element instanceof ArrayLiteral) {
is_assignable = isArrayAssignable(required_element_type, element);
} else {
is_assignable = element.variables[0] ? isAssignable(required_element_type, element.variables[0]) : false;
}
} else {
// base type = the element must match the (non-array) type
if (element instanceof ArrayLiteral) {
is_assignable = false;
} else {
is_assignable = element.variables[0] ? isAssignable(required_element_type, element.variables[0]) : false;
}
}
if (!is_assignable) {
return false;
}
}
return true;
}
/**
*
* @param {JavaType} type
* @param {Local|Parameter|Field|ArrayElement|Value} value
*/
function isAssignable(type, value) {
if (value instanceof LiteralNumber) {
return value.isCompatibleWith(type);
}
return isTypeAssignable(type, value.type);
}
/**
* @param {JavaType} source_type
* @param {JavaType} cast_type
*/
function isTypeCastable(source_type, cast_type) {
if (source_type.typeSignature === 'Ljava/lang/Object;') {
// everything is castable from Object
return true;
}
if (cast_type.typeSignature === 'Ljava/lang/Object;') {
// everything is castable to Object
return true;
}
if (source_type instanceof CEIType && cast_type instanceof CEIType) {
if (source_type.typeKind === 'interface') {
// interfaces are castable to any non-final class type (derived types might implement the interface)
if (cast_type.typeKind === 'class' && !cast_type.modifiers.includes('final')) {
return true;
}
}
// for other class casts, one type must be in the inheritence tree of the other
if (getTypeInheritanceList(source_type).includes(cast_type)) {
return true;
}
if (getTypeInheritanceList(cast_type).includes(source_type)) {
return true;
}
return false;
}
if (cast_type instanceof PrimitiveType) {
// source type must be a compatible primitive or class
switch (cast_type.typeSignature) {
case 'B':
case 'S':
case 'I':
case 'J': return /^([BSIJCFD]|Ljava\/lang\/(Byte|Short|Integer|Long|Character);)$/.test(source_type.typeSignature);
case 'F':
case 'D': return /^([BSIJCFD]|Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|Double);)$/.test(source_type.typeSignature);
case 'Z': return /^([Z]|Ljava\/lang\/(Boolean);)$/.test(source_type.typeSignature);
}
return false;
}
if (cast_type instanceof ArrayType) {
// the source type must have the same array dimensionality and have a castable base type
if (source_type instanceof ArrayType) {
if (source_type.arrdims === cast_type.arrdims) {
if (isTypeCastable(source_type.base, cast_type.base)) {
return true;
}
}
}
}
if (source_type instanceof AnyType || cast_type instanceof AnyType) {
return true;
}
return false;
}
/**
* Set of regexes to map source primitives to their destination types.
* eg, long (J) is type-assignable to long, float and double (and their boxed counterparts)
* Note that void (V) is never type-assignable to anything
*/
const valid_primitive_dest_types = {
I: /^[IJFD]$|^Ljava\/lang\/(Integer|Long|Float|Double);$/,
J: /^[JFD]$|^Ljava\/lang\/(Long|Float|Double);$/,
S: /^[SIJFD]$|^Ljava\/lang\/(Short|Integer|Long|Float|Double);$/,
B: /^[BSIJFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double);$/,
F: /^[FD]$|^Ljava\/lang\/(Float|Double);$/,
D: /^D$|^Ljava\/lang\/(Double);$/,
C: /^C$|^Ljava\/lang\/(Character);$/,
Z: /^Z$|^Ljava\/lang\/(Boolean);$/,
V: /$^/, // V.test() always returns false
}
/**
* Returns true if a value of value_type is assignable to a variable of dest_type
* @param {JavaType} dest_type
* @param {JavaType} value_type
*/
function isTypeAssignable(dest_type, value_type) {
let is_assignable = false;
if (dest_type.typeSignature === value_type.typeSignature) {
// exact signature match
is_assignable = true;
} else if (dest_type instanceof AnyType || value_type instanceof AnyType) {
// everything is assignable to or from AnyType
is_assignable = true;
} else if (dest_type.rawTypeSignature === 'Ljava/lang/Object;') {
// everything is assignable to Object
is_assignable = true;
} else if (value_type instanceof PrimitiveType) {
// primitives can only be assinged to other widening primitives or their class equivilents
is_assignable = valid_primitive_dest_types[value_type.typeSignature].test(dest_type.typeSignature);
} else if (value_type instanceof NullType) {
// null is assignable to any non-primitive
is_assignable = !(dest_type instanceof PrimitiveType);
} else if (value_type instanceof ArrayType) {
// arrays are assignable to other arrays with the same dimensionality and type-assignable bases
is_assignable = dest_type instanceof ArrayType
&& dest_type.arrdims === value_type.arrdims
&& isTypeAssignable(dest_type.base, value_type.base);
} else if (value_type instanceof CEIType && dest_type instanceof CEIType) {
// class/interfaces types are assignable to any class/interface types in their inheritence tree
const valid_types = getTypeInheritanceList(value_type);
is_assignable = valid_types.includes(dest_type);
if (!is_assignable) {
// generic types are also assignable to their raw counterparts
const valid_raw_types = valid_types.map(t => t.getRawType());
is_assignable = valid_raw_types.includes(dest_type);
}
}
return is_assignable;
}
/**
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveEquality(tokens, ident, lhs, op, rhs) {
if (lhs.variables[0] && rhs.variables[0]) {
checkEqualityComparison(tokens, lhs.variables[0], op, rhs.variables[0]);
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, PrimitiveType.map.Z)]);
}
/**
* @param {TokenList} tokens
* @param {Local|Parameter|Field|ArrayElement|Value} lhs
* @param {Token} op
* @param {Local|Parameter|Field|ArrayElement|Value} rhs
*/
function checkEqualityComparison(tokens, lhs, op, rhs) {
let is_comparable;
if (lhs.type.typeSignature === rhs.type.typeSignature) {
is_comparable = true;
} else if (lhs.type instanceof AnyType || rhs.type instanceof AnyType) {
is_comparable = true;
} else if (lhs.type instanceof PrimitiveType) {
const valid_rhs_type = {
Z: /^Z$/,
V: /^$/,
}[lhs.type.typeSignature] || /^[BSIJFDC]$/;
is_comparable = valid_rhs_type.test(rhs.type.typeSignature);
} else if (lhs.type instanceof NullType || rhs.type instanceof NullType) {
is_comparable = !(rhs.type instanceof PrimitiveType);
} else if (lhs.type instanceof ArrayType) {
const base_type = lhs.type.base;
const valid_array_types = base_type instanceof CEIType ? getTypeInheritanceList(base_type) : [base_type];
is_comparable = rhs.type.typeSignature === 'Ljava/lang/Object;'
|| (rhs.type instanceof ArrayType
&& rhs.type.arrdims === rhs.type.arrdims
&& valid_array_types.includes(rhs.type));
} else if (lhs.type instanceof CEIType && rhs.type instanceof CEIType) {
const lhs_types = getTypeInheritanceList(lhs.type);
const rhs_types = getTypeInheritanceList(rhs.type);
is_comparable = lhs_types.includes(rhs.type) || rhs_types.includes(lhs.type);
}
if (!is_comparable) {
addproblem(tokens, ParseProblem.Error(op, `Incomparable types: '${lhs.type.fullyDottedTypeName}' and '${rhs.type.fullyDottedTypeName}'`));
}
// warn about comparing strings
if (lhs.type.typeSignature === 'Ljava/lang/String;' && rhs.type.typeSignature === 'Ljava/lang/String;') {
addproblem(tokens, ParseProblem.Warning(op, `String comparisons using '==' or '!=' do not produce consistent results. Consider using 'String.equals(String other)' instead.`));
}
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveComparison(tokens, ident, lhs, op, rhs) {
if (lhs.variables[0] && rhs.variables[0]) {
checkOperator(tokens, lhs.variables[0], op, rhs.variables[0], /^[BSIJFDC]{2}$/);
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, PrimitiveType.map.Z)]);
}
/**
* @param {TokenList} tokens
* @param {Local|Parameter|Field|ArrayElement|Value} lhs
* @param {Token} op
* @param {Local|Parameter|Field|ArrayElement|Value} rhs
*/
function checkOperator(tokens, lhs, op, rhs, re) {
if (lhs.type instanceof AnyType || rhs.type instanceof AnyType) {
return;
}
let is_comparable = re.test(`${lhs.type.typeSignature}${rhs.type.typeSignature}`);
if (!is_comparable) {
addproblem(tokens, ParseProblem.Error(op, `Operator ${op.value} cannot be applied to types '${lhs.type.fullyDottedTypeName}' and '${rhs.type.fullyDottedTypeName}'`));
}
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveBitwise(tokens, ident, lhs, op, rhs) {
let type = PrimitiveType.map.I;
const lhsvar = lhs.variables[0], rhsvar = rhs.variables[0];
if (lhsvar && rhsvar) {
// ^&| are both bitwse and logical operators
checkOperator(tokens, lhsvar, op, rhsvar, /^[BSIJCZ]{2}$/);
if (lhsvar.type.typeSignature === 'Z') {
type = PrimitiveType.map.Z;
}
else if (lhsvar instanceof LiteralNumber && rhsvar instanceof LiteralNumber) {
const result = LiteralNumber[op.value](lhsvar, rhsvar);
if (result) {
return new ResolvedIdent(ident, [result]);
}
}
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, type)]);
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveShift(tokens, ident, lhs, op, rhs) {
const lhsvar = lhs.variables[0], rhsvar = rhs.variables[0];
if (lhsvar && rhsvar) {
// ^&| are both bitwse and logical operators
checkOperator(tokens, lhsvar, op, rhsvar, /^[BSIJC]{2}$/);
if (lhsvar instanceof LiteralNumber && rhsvar instanceof LiteralNumber) {
const result = LiteralNumber[op.value](lhsvar, rhsvar);
if (result) {
return new ResolvedIdent(ident, [result]);
}
}
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, PrimitiveType.map.I)]);
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveLogical(tokens, ident, lhs, op, rhs) {
if (lhs.variables[0] && rhs.variables[0]) {
checkOperator(tokens, lhs.variables[0], op, rhs.variables[0], /^ZZ$/);
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, PrimitiveType.map.Z)]);
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveInstanceOf(tokens, ident, lhs, op, rhs) {
if (!rhs.types[0]) {
addproblem(tokens, ParseProblem.Error(op, `Operator instanceof requires a type name for comparison.`));
}
return new ResolvedIdent(ident, [new Value(ident, PrimitiveType.map.Z)]);
}
/**
* @param {TokenList} tokens
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
function resolveMath(tokens, ident, lhs, op, rhs) {
const lhsvar = lhs.variables[0], rhsvar = rhs.variables[0];
if (!lhsvar || !rhsvar) {
return new ResolvedIdent(ident);
}
if (op.value === '+') {
// if either side of the + is a string, the result is a string
for (let operand of [lhs, rhs])
if (operand.variables[0].type.typeSignature === 'Ljava/lang/String;') {
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, operand.variables[0].type)]);
}
}
checkOperator(tokens, lhsvar, op, rhsvar, /^[BISJFDC]{2}$/);
if (lhsvar instanceof LiteralNumber && rhsvar instanceof LiteralNumber) {
const result = LiteralNumber[op.value](lhsvar, rhsvar);
if (result) {
return new ResolvedIdent(ident, [result]);
}
}
/** @type {JavaType} */
let type;
const typekey = `${lhsvar.type.typeSignature}${rhsvar.type.typeSignature}`;
const lhtypematches = 'SB,IB,JB,FB,DB,IS,JS,FS,DS,JI,FI,DI,FJ,DJ,DF';
if (lhtypematches.indexOf(typekey) >= 0) {
type = lhsvar.type;
} else if (/^(C.|.C)$/.test(typekey)) {
type = PrimitiveType.map.I;
} else {
type = rhsvar.type;
}
return new ResolvedIdent(ident, [Value.build(ident, lhs, rhs, type)]);
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function qualifiedTerm(tokens, locals, method, imports, typemap) {
let matches = rootTerm(tokens, locals, method, imports, typemap);
if (tokens.current.kind === 'inc-operator') {
// postfix inc/dec - only applies to assignable number variables and no qualifiers are allowed to follow
const postfix_operator = tokens.current;
tokens.inc();
const vars = matches.variables.filter(v => /^[BSIJFD]$/.test(v.type.typeSignature))
if (!vars[0]) {
addproblem(tokens, ParseProblem.Error(postfix_operator, `Postfix operator cannot be specified here`));
}
return new ResolvedIdent(`${matches.source}${postfix_operator.value}`, vars);
}
matches = qualifiers(matches, tokens, locals, method, imports, typemap);
return matches;
}
/**
*
* @param {Token} token
*/
function isExpressionStart(token) {
return /^(ident|primitive-type|[\w-]+-literal|(inc|plumin|unary)-operator|open-bracket|new-operator)$/.test(token.kind);
}
/**
* @param {Token} token first token following the close bracket
* @param {ResolvedIdent} matches - the bracketed expression
*/
function isCastExpression(token, matches) {
// working out if this is supposed to be a cast expression is problematic.
// (a) + b -> cast or binary expression (depends on how a is resolved)
// if the bracketed expression cannot be resolved:
// (a) b -> assumed to be a cast
// (a) + b -> assumed to be an expression
// (a) 5 -> assumed to be a cast
// (a) + 5 -> assumed to be an expression
if (matches.types[0] && !(matches.types[0] instanceof AnyType)) {
// resolved type - this must be a cast
return true;
}
if (!matches.types[0]) {
// not a type - this must be an expression
return false;
}
// if we reach here, the type is AnyType - we assume a cast if the next
// value is the start of an expression, except for +/-
if (token.kind === 'plumin-operator') {
return false;
}
return this.isExpressionStart(token);
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
* @returns {ResolvedIdent}
*/
function rootTerm(tokens, locals, method, imports, typemap) {
/** @type {ResolvedIdent} */
let matches;
switch(tokens.current.kind) {
case 'ident':
matches = resolveIdentifier(tokens, locals, method, imports, typemap);
break;
case 'primitive-type':
matches = new ResolvedIdent(tokens.current.value, [], [], [PrimitiveType.fromName(tokens.current.value)]);
break;
case 'string-literal':
matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, typemap.get('java/lang/String'))]);
break;
case 'char-literal':
matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, PrimitiveType.map.C)]);
break;
case 'boolean-literal':
matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, PrimitiveType.map.Z)]);
break;
case 'object-literal':
// this, super or null
if (tokens.current.value === 'this') {
matches = new ResolvedIdent(tokens.current.value, [new Value(tokens.current.value, method._owner)]);
} else if (tokens.current.value === 'super') {
const supertype = method._owner.supers.find(s => s.typeKind === 'class') || typemap.get('java/lang/Object');
matches = new ResolvedIdent(tokens.current.value, [new Value(tokens.current.value, supertype)]);
} else {
matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, new NullType())]);
}
break;
case /number-literal/.test(tokens.current.kind) && tokens.current.kind:
matches = new ResolvedIdent(tokens.current.value, [LiteralNumber.from(tokens.current)]);
break;
case 'inc-operator':
let incop = tokens.current;
tokens.inc();
matches = qualifiedTerm(tokens, locals, method, imports, typemap);
const inc_ident = `${incop.value}${matches.source}`;
if (!matches.variables[0]) {
return new ResolvedIdent(inc_ident);
}
if (matches.variables[0] instanceof Value) {
addproblem(tokens, ParseProblem.Error(incop, `${incop.value} operator is not valid`));
}
return new ResolvedIdent(inc_ident, [new Value(inc_ident, matches.variables[0].type)]);
case 'plumin-operator':
case 'unary-operator':
tokens.inc();
return qualifiedTerm(tokens, locals, method, imports, typemap);
case 'new-operator':
tokens.inc();
const ctr = qualifiedTerm(tokens, locals, method, imports, typemap);
let new_ident = `new ${ctr.source}`;
if (ctr.types[0] instanceof ArrayType) {
if (tokens.current.value === '{') {
// array init
rootTerm(tokens, locals, method, imports, typemap);
}
return new ResolvedIdent(new_ident, [new Value(new_ident, ctr.types[0])]);
}
if (ctr.variables[0] instanceof ConstructorCall) {
const ctr_type = ctr.variables[0].type;
if (tokens.current.value === '{') {
// final types cannot be inherited
if (ctr_type.modifiers.includes('final') ) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Type '${ctr_type.fullyDottedTypeName}' is declared final and cannot be inherited from.`));
}
// anonymous type - just skip for now
for (let balance = 0;;) {
if (tokens.isValue('{')) {
balance++;
} else if (tokens.isValue('}')) {
if (--balance === 0) {
break;
}
} else tokens.inc();
}
} else {
// abstract and interface types must have a type body
if (ctr_type.typeKind === 'interface' || ctr_type.modifiers.includes('abstract') ) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Type '${ctr_type.fullyDottedTypeName}' is abstract and cannot be instantiated without a body`));
}
}
return new ResolvedIdent(new_ident, [new Value(new_ident, ctr.variables[0].type)]);
}
addproblem(tokens, ParseProblem.Error(tokens.current, 'Constructor expression expected'));
return new ResolvedIdent(new_ident);
case 'open-bracket':
tokens.inc();
matches = expression(tokens, locals, method, imports, typemap);
const close_bracket = tokens.current;
tokens.expectValue(')');
if (isCastExpression(tokens.current, matches)) {
// typecast
const type = matches.types[0];
if (!type) {
addproblem(tokens, ParseProblem.Error(close_bracket, 'Type expected'));
}
const cast_matches = qualifiedTerm(tokens, locals, method, imports, typemap)
// cast any variables as values with the new type
const vars = cast_matches.variables.map(v => {
if (type && !isTypeCastable(v.type, type)) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Expression of type '${v.type.fullyDottedTypeName}' cannot be cast to type '${type.fullyDottedTypeName}'`));
}
return new Value(v.name, type || v.type);
});
return new ResolvedIdent(`(${matches.source})${cast_matches.source}`, vars);
}
// the result of a bracketed expression is always a value, never a variable
// - this prevents things like: (a) = 5;
const vars = matches.variables.map((v, i, arr) => arr[i] = v instanceof Value ? v : new Value(v.name, v.type));
return new ResolvedIdent(`(${matches.source})`, vars);
case tokens.isValue('{') && 'symbol':
// array initer
let elements = [];
if (!tokens.isValue('}')) {
elements = expressionList(tokens, locals, method, imports, typemap);
tokens.expectValue('}');
}
const ident = `{${elements.map(e => e.source).join(',')}}`;
return new ResolvedIdent(ident, [new ArrayLiteral(ident, elements)]);
default:
addproblem(tokens, ParseProblem.Error(tokens.current, 'Expression expected'));
return new ResolvedIdent('');
}
tokens.inc();
return matches;
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function expressionList(tokens, locals, method, imports, typemap) {
let e = expression(tokens, locals, method, imports, typemap);
const expressions = [e];
while (tokens.current.value === ',') {
tokens.inc();
e = expression(tokens, locals, method, imports, typemap);
expressions.push(e);
}
return expressions;
}
/**
* @param {TokenList} tokens
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function typeIdentList(tokens, method, imports, typemap) {
let type = typeIdent(tokens, method, imports, typemap);
const types = [type];
while (tokens.current.value === ',') {
tokens.inc();
type = typeIdent(tokens, method, imports, typemap);
types.push(type);
}
return types;
}
/**
* @param {TokenList} tokens
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function typeIdent(tokens, method, imports, typemap) {
if (tokens.current.kind !== 'ident') {
return new UnresolvedType();
}
const { types, package_name } = resolveTypeOrPackage(tokens.current.value, method._owner, imports, typemap);
let matches = new ResolvedIdent(tokens.current.value, [], [], types, package_name);
for (;;) {
tokens.inc();
if (tokens.isValue('.')) {
matches = parseDottedIdent(matches, tokens, typemap);
} else if (tokens.isValue('<')) {
if (!tokens.isValue('>')) {
typeIdentList(tokens, method, imports, typemap);
tokens.expectValue('>');
}
} else {
break;
}
}
return matches.types[0] || new UnresolvedType(matches.source);
}
/**
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function arrayIndexOrDimension(tokens, locals, method, imports, typemap) {
let e = expression(tokens, locals, method, imports, typemap);
// the value must be a integer-compatible
const values = e.variables.map(v => new Value(v.name, v.type)).filter(v => /^[BIS]$/.test(v.type.typeSignature));
if (!values[0]) {
addproblem(tokens, ParseProblem.Error(tokens.current, 'Invalid array index expression'));
}
return new ResolvedIdent(e.source, values);
}
/**
* @param {TokenList} tokens
* @param {Token} open_array
* @param {ResolvedIdent} matches
* @param {ResolvedIdent} index
*/
function arrayElementOrConstructor(tokens, open_array, matches, index) {
const ident = `${matches.source}[${index.source}]`;
// we must have an array-type variable or at least one type
const variables = matches.variables
.filter(v => v.type instanceof ArrayType)
.map(v => new ArrayElement(v, index));
const types = matches.types.map(t => t instanceof ArrayType ? new ArrayType(t.base, t.arrdims+1) : new ArrayType(t, 1));
if (!variables[0] && !types[0]) {
addproblem(tokens, ParseProblem.Error(open_array, `Invalid array expression`));
}
return new ResolvedIdent(ident, variables, [], types);
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} instance
* @param {ResolvedIdent[]} call_arguments
* @param {Map<String, JavaType>} typemap
*/
function methodCallExpression(tokens, instance, call_arguments, typemap) {
const ident = `${instance.source}(${call_arguments.map(arg => arg.source).join(',')})`;
// method call resolving is painful in Java - we need to match arguments against
// possible types in the call, but this must include matching against inherited types and choosing the
// most-specific match
const methods = instance.methods.filter(m => isCallCompatible(m, call_arguments));
const types = instance.types.filter(t => {
// interfaces use Object constructors
const type = t.typeKind === 'interface'
? typemap.get('java/lang/Object')
: t;
return type.constructors.find(c => isCallCompatible(c, call_arguments));
});
if (!types[0] && !methods[0]) {
const callargtypes = call_arguments.map(a => a.variables[0] ? a.variables[0].type.fullyDottedTypeName : '<unknown-type>').join(', ');
if (instance.methods[0]) {
const methodlist = instance.methods.map(m => m.label).join('\n- ');
addproblem(tokens, ParseProblem.Error(tokens.current,
`No compatible method found. Tried to match:\n- ${methodlist}\nagainst call argument types: (${callargtypes})`))
// fake a result with AnyMethod
methods.push(new AnyMethod(instance.source));
} else if (instance.types[0]) {
const ctrlist = instance.types[0].constructors.map(c => c.label).join('\n- ');
const match_message = instance.types[0].constructors.length
? `Tried to match:\n- ${ctrlist}\nagainst call argument types: (${callargtypes})`
: 'The type has no accessible constructors';
addproblem(tokens, ParseProblem.Error(tokens.current,
`No compatible constructor found for type '${instance.types[0].fullyDottedTypeName}'. ${match_message}`));
// fake a result with AnyType
types.push(new AnyType(instance.source));
}
}
// the result is a value of the return type of the method or the type
const variables = [
...methods.map(m => new MethodCall(ident, instance, m)),
...types.map(t => new ConstructorCall(ident, t))
];
return new ResolvedIdent(ident, variables);
}
/**
* Returns true if the set of call arguments are assignable to the method or constructor parameters
* @param {Method|Constructor} m
* @param {ResolvedIdent[]} call_arguments
*/
function isCallCompatible(m, call_arguments) {
if (m instanceof AnyMethod) {
return true;
}
if (m.parameterCount !== call_arguments.length) {
// wrong parameter count - this needs updating to support varargs
return false;
}
const p = m.parameters;
for (let i=0; i < p.length; i++) {
if (!call_arguments[i].variables[0]) {
// only variables can be passed - not types or methods
return false;
}
// is the argument assignable to the parameter
if (isAssignable(p[i].type, call_arguments[i].variables[0])) {
continue;
}
// mismatch parameter type
return;
}
return true;
}
/**
* @param {CEIType} type
*/
function getTypeInheritanceList(type) {
const types = {
/** @type {JavaType[]} */
list: [type],
/** @type {Set<JavaType>} */
done: new Set(),
};
for (let type; type = types.list.shift(); ) {
if (types.done.has(type)) {
continue;
}
types.done.add(type);
if (type instanceof CEIType)
types.list.push(...type.supers);
}
return Array.from(types.done);
}
class NullType extends JavaType {
constructor() {
super('class', [], '');
super.simpleTypeName = 'null';
}
get typeSignature() {
return 'null';
}
}
/**
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function qualifiers(matches, tokens, locals, method, imports, typemap) {
for (;;) {
switch (tokens.current.value) {
case '.':
tokens.inc();
matches = parseDottedIdent(matches, tokens, typemap);
break;
case '[':
let open_array = tokens.current;
if (tokens.inc().value === ']') {
// array type
tokens.inc();
matches = arrayTypeExpression(matches);
} else {
// array index
const index = arrayIndexOrDimension(tokens, locals, method, imports, typemap);
matches = arrayElementOrConstructor(tokens, open_array, matches, index);
// @ts-ignore
tokens.expectValue(']');
}
break;
case '(':
// method or constructor call
let args = [];
if (tokens.inc().value === ')') {
tokens.inc();
} else {
args = expressionList(tokens, locals, method, imports, typemap);
tokens.expectValue(')');
}
matches = methodCallExpression(tokens, matches, args, typemap);
break;
case '<':
// generic type arguments - since this can be confused with less-than, only parse
// it if there is at least one type and no matching variables
if (!matches.types[0] || matches.variables[0]) {
return matches;
}
tokens.inc();
let type_arguments = [];
if (!tokens.isValue('>')) {
type_arguments = typeIdentList(tokens, method, imports, typemap);
tokens.expectValue('>');
}
matches.types = matches.types.map(t => {
if (t instanceof CEIType) {
if (t.typevars.length) {
const specialised_type = t.specialise(type_arguments);
typemap.set(specialised_type.shortSignature, specialised_type);
return specialised_type;
}
}
return t;
});
break;
default:
return matches;
}
}
}
/**
* @param {ResolvedIdent} matches
*/
function arrayTypeExpression(matches) {
const types = matches.types.map(t => {
if (t instanceof ArrayType) {
return new ArrayType(t.base, t.arrdims + 1);
}
return new ArrayType(t, 1);
});
return new ResolvedIdent(`${matches.source}[]`, [], [], types);
}
/**
*
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {Map<string,JavaType>} typemap
*/
function parseDottedIdent(matches, tokens, typemap) {
let variables = [],
methods = [],
types = [],
package_name = '';
const qualified_ident = `${matches.source}.${tokens.current.value}`;
switch (tokens.current.value) {
case 'class':
// e.g int.class
// convert the types to Class instances
tokens.inc();
variables = matches.types.map(t => {
const type_signature = t instanceof AnyType ? '' : `<${t.typeSignature}>`
return new Value(qualified_ident, signatureToType(`Ljava/lang/Class${type_signature};`, typemap));
});
return new ResolvedIdent(qualified_ident, variables);
case 'this':
// e.g Type.this - it must be an enclosing type
// convert the types to 'this' instances
tokens.inc();
variables = matches.types.map(t => new Value(qualified_ident, t));
return new ResolvedIdent(qualified_ident, variables);
}
if (tokens.current.kind !== 'ident') {
addproblem(tokens, ParseProblem.Error(tokens.current, 'Identifier expected'));
return matches;
}
matches.source = qualified_ident;
// the ident could be a field, method, type or package qualifier
matches.variables.forEach(v => {
const decls = v.type.findDeclsByName(tokens.current.value);
variables.push(...decls.fields);
methods.push(...decls.methods);
});
/** @type {JavaType[]} */
matches.types.forEach(t => {
// if there is an AnyType, then add a type, variable and method
// - this prevents multiple errors in dotted values/
// e.g R.layout.name wiil only error once (on R), not on all 3 idents
if (t instanceof AnyType) {
types.push(new AnyType(qualified_ident));
variables.push(new AnyValue(qualified_ident));
methods.push(new AnyMethod(tokens.current.value));
return;
}
if (t instanceof CEIType) {
const enclosed_type_signature = `${t.shortSignature}$${tokens.current.value}`;
const enc_type = typemap.get(enclosed_type_signature);
if (enc_type) {
types.push(enc_type);
}
}
// search static fields and methods
const decls = t.findDeclsByName(tokens.current.value);
variables.push(...decls.fields);
methods.push(...decls.methods);
});
if (matches.package_name) {
// if there is a package name, the ident could represent a sub-package or a top-leve type name
const type_match = `${matches.package_name}/${tokens.current.value}`;
if (typemap.has(type_match)) {
// it matches a type
types.push(typemap.get(type_match));
} else {
const package_match = `${matches.package_name}/${tokens.current.value}/`;
if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) {
package_name = type_match;
}
}
}
const match = new ResolvedIdent(qualified_ident, variables, methods, types, package_name);
checkIdentifierFound(tokens, tokens.current.value, match);
tokens.inc();
return match;
}
/**
* When resolving identifiers, we need to search across everything because
* identifiers are context-sensitive.
* For example, the following compiles even though C takes on different definitions within method:
*
* class A {
* class C {
* }
* }
*
* class B extends A {
* String C;
* int C() {
* return C.length();
* }
* void method() {
* C obj = new C();
* int x = C.class.getName().length() + C.length() + C();
* }
* }
*
* But... parameters and locals override fields and methods (and local types override enclosed types)
*
* @param {TokenList} tokens
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function resolveIdentifier(tokens, locals, method, imports, typemap) {
const ident = tokens.current.value;
const matches = findIdentifier(ident, locals, method, imports, typemap);
checkIdentifierFound(tokens, ident, matches);
return matches;
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} matches
*/
function checkIdentifierFound(tokens, ident, matches) {
if (!matches.variables[0] && !matches.methods[0] && !matches.types[0] && !matches.package_name) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Unresolved identifier: ${matches.source}`));
// pretend it matches everything
matches.variables = [new AnyValue(matches.source)];
matches.methods = [new AnyMethod(ident)];
matches.types = [new AnyType(matches.source)];
}
}
/**
* @param {string} ident
* @param {Local[]} locals
* @param {SourceMC} method
* @param {ResolvedImport[]} imports
* @param {Map<String,JavaType>} typemap
*/
function findIdentifier(ident, locals, method, imports, typemap) {
const matches = new ResolvedIdent(ident);
// is it a local or parameter - note that locals must be ordered innermost-scope-first
const local = locals.find(local => local.name === ident);
const param = method.parameters.find(p => p.name === ident);
if (local || param) {
matches.variables = [local || param];
} else {
// is it a field or method in the current type (or any of the superclasses)
const types = getTypeInheritanceList(method._owner);
const method_sigs = new Set();
types.forEach(type => {
if (!matches.variables[0]) {
const field = type.fields.find(f => f.name === ident);
if (field) {
matches.variables = [field];
}
}
matches.methods = matches.methods.concat(
type.methods.filter(m => {
if (m.name !== ident || method_sigs.has(m.methodSignature)) {
return;
}
method_sigs.add(m.methodSignature);
return true;
})
);
});
}
const { types, package_name } = resolveTypeOrPackage(ident, method._owner, imports, typemap);
matches.types = types;
matches.package_name = package_name;
return matches;
}
/**
*
* @param {string} ident
* @param {CEIType} scoped_type
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function resolveTypeOrPackage(ident, scoped_type, imports, typemap) {
const types = [];
let package_name = '';
// is it an enclosed type of the currently scoped type or any outer type
if (scoped_type) {
const scopes = scoped_type.shortSignature.split('$');
while (scopes.length) {
const enc_type = typemap.get(`${scopes.join('$')}$${ident}`);
if (enc_type) {
types.push(enc_type);
break;
}
scopes.pop();
}
}
if (!types[0]) {
// is it a top-level type from the imports
const top_level_type = '/' + ident;
for (let i of imports) {
const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(top_level_type));
if (fqn) {
types.push(i.types.get(fqn));
}
}
}
if (!types[0]) {
// is it a default-package type
const default_type = typemap.get(ident);
if (default_type) {
types.push(default_type);
}
}
// the final option is the start of a package name
const package_root = ident + '/';
const typelist = [...typemap.keys()];
if (typelist.find(fqn => fqn.startsWith(package_root))) {
package_name = ident;
}
return {
types,
package_name,
}
}
/**
* AnyType is a special type that's used to fill in types that are missing.
* To prevent cascading errors, AnyType should be fully assign/cast/type-compatible
* with any other type
*/
class AnyType extends JavaType {
/**
*
* @param {String} label
*/
constructor(label) {
super("class", [], '');
super.simpleTypeName = label;
}
static Instance = new AnyType('');
get rawTypeSignature() {
return 'U';
}
get typeSignature() {
return 'U';
}
}
class AnyMethod extends Method {
/**
* @param {string} name
*/
constructor(name) {
super(name, [], '');
}
get returnType() {
return AnyType.Instance;
}
}
class Local {
/**
* @param {Token[]} modifiers
* @param {string} name
* @param {Token} decltoken
* @param {JavaType} type
*/
constructor(modifiers, name, decltoken, type) {
this.finalToken = modifiers.find(m => m.source === 'final') || null;
this.name = name;
this.decltoken = decltoken;
this.type = type;
this.init = null;
}
}
class ArrayElement {
/**
*
* @param {Local|Parameter|Field|ArrayElement|Value} array_variable
* @param {ResolvedIdent} index
*/
constructor(array_variable, index) {
this.array_variable = array_variable;
this.index = index;
if (!(this.array_variable.type instanceof ArrayType)) {
throw new Error('Array element cannot be created from non-array type');
}
this.name = `${array_variable.name}[${index.source}]`;
/** @type {JavaType} */
this.type = this.array_variable.type.elementType;
}
}
class Value {
/**
* @param {string} name
* @param {JavaType} type
*/
constructor(name, type) {
this.name = name;
this.type = type;
}
/**
* @param {string} ident
* @param {ResolvedIdent} lhs
* @param {ResolvedIdent} rhs
* @param {JavaType} type
*/
static build(ident, lhs, rhs, type) {
if (!lhs.variables[0] || !rhs.variables[0]) {
return new Value(ident, type);
}
if (lhs.variables[0] instanceof LiteralValue && rhs.variables && rhs.variables[0] instanceof LiteralValue) {
new LiteralValue(ident, type);
}
return new Value(ident, type);
}
}
class AnyValue extends Value {
constructor(name) {
super(name, AnyType.Instance);
}
}
class LiteralValue extends Value { }
/**
* LiteralNumberType is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc).
*
* It's used to allow literal numbers to be type-assignable to variables with different primitive types.
* For example, 200 is type-assignable to short, int, long, float and double, but not byte.
*/
class LiteralNumber extends LiteralValue {
/**
* @param {string} value
* @param {string} kind
* @param {PrimitiveType} default_type
*/
constructor(value, kind, default_type) {
super(value, default_type);
this.numberValue = value;
this.numberKind = kind;
}
static shift(a, b, op) {
const ai = a.toInt(), bi = b.toInt();
if (ai === null || bi === null) {
return null;
}
const val = op(ai, bi);
const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I;
return new LiteralNumber(val.toString(), 'int-number-literal', type);
}
static bitwise(a, b, op) {
const ai = a.toInt(), bi = b.toInt();
if (ai === null || bi === null) {
return null;
}
const val = op(ai, bi);
const typekey = a.type.typeSignature+ b.type.typeSignature;
let type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I;
return new LiteralNumber(val.toString(), 'int-number-literal', type);
}
static math(a, b, op, divmod) {
const ai = a.toNumber(), bi = b.toNumber();
if (bi === 0 && divmod) {
return null;
}
let val = op(ai, bi);
const typekey = a.type.typeSignature+ b.type.typeSignature;
if (!/[FD]/.test(typekey) && divmod) {
val = Math.trunc(val);
}
let type;
if (/^(D|F[^D]|J[^FD])/.test(typekey)) {
type = a.type;
} else {
type = b.type;
}
return new LiteralNumber(val.toString(), 'int-number-literal', type);
}
static '+'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a + b) }
static '-'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a - b) }
static '*'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a * b) }
static '/'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a / b, true) }
static '%'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a % b, true) }
static '&'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a & b) }
static '|'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a | b) }
static '^'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a ^ b) }
static '>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a >> b) }
static '>>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => {
// unsigned shift (>>>) is not supported by bigints
// @ts-ignore
return (a >> b) & ~(-1n << (64n - b));
}) }
static '<<'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a << b) }
toInt() {
switch (this.numberKind) {
case 'hex-number-literal':
case 'int-number-literal':
return BigInt(this.name);
}
return null;
}
toNumber() {
return parseFloat(this.name);
}
/**
* @param {JavaType} type
*/
isCompatibleWith(type) {
if (this.type === type) {
return true;
}
switch(this.type.simpleTypeName) {
case 'double':
return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature);
case 'float':
return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature);
}
// all integral types are all compatible with long, float and double variables
if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) {
return true;
}
// the desintation type must be a number primitive or one of the corresponding boxed classes
if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) {
return false;
}
let number = 0;
if (this.numberKind === 'hex-number-literal') {
if (this.numberValue !== '0x') {
const non_leading_zero_digits = this.numberValue.match(/0x0*(.+)/)[1];
number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16);
}
} else if (this.numberKind === 'int-number-literal') {
const non_leading_zero_digits = this.numberValue.match(/0*(.+)/)[1];
number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10);
}
if (number >= -128 && number <= 127) {
return true; // byte values are compatible with all other numbers
}
if (number >= -32768 && number <= 32767) {
return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte
}
return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character
}
/**
* @param {Token} token
*/
static from(token) {
function suffix(which) {
switch(which.indexOf(token.value.slice(-1))) {
case 0:
case 1:
return PrimitiveType.map.F;
case 2:
case 3:
return PrimitiveType.map.D;
case 4:
case 5:
return PrimitiveType.map.J;
}
}
switch(token.kind) {
case 'dec-exp-number-literal':
case 'dec-number-literal':
return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.D);
case 'hex-number-literal':
return new LiteralNumber(token.value, token.kind, suffix(' Ll') || PrimitiveType.map.I);
case 'int-number-literal':
default:
return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.I);
}
}
}
class MethodCall extends Value {
/**
* @param {string} name
* @param {ResolvedIdent} instance
* @param {Method} method
*/
constructor(name, instance, method) {
super(name, method.returnType);
this.instance = instance;
this.method = method;
}
}
class ConstructorCall extends Value {
/**
* @param {string} name
* @param {JavaType} type
*/
constructor(name, type) {
super(name, type);
}
}
class ArrayLiteral extends LiteralValue {
/**
* @param {string} name
* @param {ResolvedIdent[]} elements
*/
constructor(name, elements) {
super(name, null);
this.elements = elements;
}
}
class TernaryValue extends Value {
/**
* @param {string} name
* @param {JavaType} true_type
* @param {Token} colon
* @param {Value} false_value
*/
constructor(name, true_type, colon, false_value) {
super(name, true_type);
this.colon = colon;
this.falseValue = false_value;
}
}
class ResolvedIdent {
/**
* @param {string} ident
* @param {(Local|Parameter|Field|ArrayElement|Value)[]} variables
* @param {Method[]} methods
* @param {JavaType[]} types
* @param {string} package_name
*/
constructor(ident, variables = [], methods = [], types = [], package_name = '') {
this.source = ident;
this.variables = variables;
this.methods = methods;
this.types = types;
this.package_name = package_name;
}
}
/**
* \s+ whitespace
* \/\/.* single-line comment (slc)
* \/\*[\d\D]*?\*\/ multi-line comment (mlc)
* "[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*" string literal - correctly terminated but may contain invalid escapes
* ".* unterminated string literal
* '\\?.?'? character literal - possibly unterminated and/or with invalid escape
* \.?\d number literal (start) - further processing extracts the value
* \w+ word - keyword or identifier
* [;,?:(){}\[\]] single-character symbols and operators
* \.(\.\.)? . ...
*
* the operators: [!=/%*^]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(=|\++)?|\-+=?
* [!=/%*^]=? ! = / % * ^ != == /= %= *= ^=
* <<?=? < << <= <<=
* >>?[>=]? > >> >= >>> >>=
* &[&=]? & && &=
* \|[|=]? | || |=
* (\+\+|--) ++ -- postfix inc - only matches if immediately preceded by a word or a ]
* [+-]=? + - += -=
*
*
*
*/
/**
*
* @param {string} source
* @param {number} [offset]
* @param {number} [length]
*/
function tokenize(source, offset = 0, length = source.length) {
const text = source.slice(offset, offset + length);
const raw_token_re = /(\s+|\/\/.*|\/\*[\d\D]*?\*\/)|("[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*")|(".*)|('\\?.?'?)|(\.?\d)|(\w+)|(\()|([;,?:(){}\[\]@]|\.(?:\.\.)?)|([!=/%*^]=?|<<?=?|>>?>?=?|&[&=]?|\|[|=]?|(\+\+|--)|[+-]=?|~)|$/g;
const raw_token_types = [
'wsc',
'string-literal',
'unterminated-string-literal',
'char-literal',
'number-literal',
'word',
'open-bracket',
'symbol',
'operator',
];
/**
* ```
* true|false boolean
* this|null object
* int|long|short|byte|float|double|char|boolean|void primitive type
* new
* instanceof
* public|private|protected|static|final|abstract|native|volatile|transient|synchronized modifier
* if|else|while|for|do|try|catch|finally|switch|case|default|return|break|continue statement keyword
* class|enum|interface type keyword
* package|import package keyword
* \w+ word
* ```
*/
const word_re = /(?:(true|false)|(this|super|null)|(int|long|short|byte|float|double|char|boolean|void)|(new)|(instanceof)|(public|private|protected|static|final|abstract|native|volatile|transient|synchronized)|(if|else|while|for|do|try|catch|finally|switch|case|default|return|break|continue|throw)|(class|enum|interface)|(package|import)|(\w+))\b/g;
const word_token_types = [
'boolean-literal',
'object-literal',
'primitive-type',
'new-operator',
'instanceof-operator',
'modifier',
'statement-kw',
'type-kw',
'package-kw',
'ident'
]
/**
* ```
* \d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]? decimal exponent: 1e0, 1.5e+10, 0.123E-20d
* (?:\d+\.\d*|\.\d+)[fFdD]? decimal number: 0.1, 12.34f, 7.D, .3
* 0x[\da-fA-F]*[lL]? hex integer: 0x1, 0xaBc, 0x, 0x7L
* \d+[fFdDlL]? integer: 0, 123, 234f, 345L
* ```
* todo - underscore seperators
*/
const number_re = /((?:\d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]?)|((?:\d+\.\d*|\.\d+)[fFdD]?)|(0x[\da-fA-F]*[lL]?)|(\d+[fFdDlL]?)/g;
const number_token_types = [
'dec-exp-number-literal',
'dec-number-literal',
'hex-number-literal',
'int-number-literal',
]
const tokens = [];
let lastindex = 0, m;
while (m = raw_token_re.exec(text)) {
// any text appearing between two matches is invalid
if (m.index > lastindex) {
tokens.push(new Token(source, offset + lastindex, m.index - lastindex, 'invalid'));
}
lastindex = m.index + m[0].length;
if (m.index >= text.length) {
// end of input
break;
}
let idx = m.findIndex((match,i) => i && match) - 1;
let tokentype = raw_token_types[idx];
switch(tokentype) {
case 'number-literal':
// we need to extract the exact number part
number_re.lastIndex = m.index;
m = number_re.exec(text);
idx = m.findIndex((match,i) => i && match) - 1;
tokentype = number_token_types[idx];
// update the raw_token_re position based on the length of the extracted number
raw_token_re.lastIndex = lastindex = number_re.lastIndex;
break;
case 'word':
// we need to work out what kind of keyword, literal or ident this is
word_re.lastIndex = m.index;
m = word_re.exec(text);
idx = m.findIndex((match,i) => i && match) - 1;
tokentype = word_token_types[idx];
break;
case 'operator':
// find the operator-type
tokentype = getOperatorType(m[0]);
break;
}
tokens.push(new Token(source, offset + m.index, m[0].length, tokentype));
}
return tokens;
}
/**
* ```
* =|[/%*&|^+-]=|>>>?=|<<= assignment
* \+\+|-- inc
* [!=]= equality
* [<>]=? comparison
* [&|^] bitwise
* <<|>>>? shift
* &&|[|][|] logical
* [*%/] muldiv
* [+-] plumin
* [~!] unary
* ```
*/
const operator_re = /^(?:(=|[/%*&|^+-]=|>>>?=|<<=)|(\+\+|--)|([!=]=)|([<>]=?)|([&|^])|(<<|>>>?)|(&&|[|][|])|([*%/])|([+-])|([~!]))$/;
/**
* @typedef {
'assignment-operator'|
'inc-operator'|
'equality-operator'|
'comparison-operator'|
'bitwise-operator'|
'shift-operator'|
'logical-operator'|
'muldiv-operator'|
'plumin-operator'|
'unary-operator'} OperatorKind
*/
/** @type {OperatorKind[]} */
const operator_token_types = [
'assignment-operator',
'inc-operator',
'equality-operator',
'comparison-operator',
'bitwise-operator',
'shift-operator',
'logical-operator',
'muldiv-operator',
'plumin-operator',
'unary-operator',
]
/**
* @param {string} value
*/
function getOperatorType(value) {
const op_match = value.match(operator_re);
const idx = op_match.findIndex((match,i) => i && match) - 1;
// @ts-ignore
return operator_token_types[idx];
}
class Token extends TextBlock {
/**
*
* @param {string} text
* @param {number} start
* @param {number} length
* @param {string} kind
*/
constructor(text, start, length, kind) {
super(new BlockRange(text, start, length), null);
this.kind = kind;
}
get value() {
return this.source;
}
}
function testTokenize() {
const tests = [
// the basics
{ src: 'i', r: [{value: 'i', kind:'ident'}] },
{ src: '0', r: [{value: '0', kind:'int-number-literal'}] },
{ src: `""`, r: [{value: `""`, kind:'string-literal'}] },
{ src: `'x'`, r: [{value: `'x'`, kind:'char-literal'}] },
{ src: `(`, r: [{value: `(`, kind:'open-bracket'}] },
...'. , [ ] ? : @'.split(' ').map(symbol => ({ src: symbol, r: [{value: symbol, kind: 'symbol'}] })),
...'= += -= *= /= %= >>= <<= &= |= ^='.split(' ').map(op => ({ src: op, r: [{value: op, kind:'assignment-operator'}] })),
...'+ -'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'plumin-operator'}] })),
...'* / %'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'muldiv-operator'}] })),
...'# ¬'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'invalid'}] })),
// numbers - decimal with exponent
...'0.0e+0 0.0E+0 0e+0 0e0 .0e0 0e0f 0e0d'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
// numbers - decimal with partial exponent
...'0.0e+ 0.0E+ 0e+ 0e .0e 0ef 0ed'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
// numbers - not decimal exponent
{ src: '0.0ea', r: [{value: '0.0e', kind:'dec-exp-number-literal'}, {value: 'a', kind:'ident'}] },
// numbers - decimal (no exponent)
...'0.123 0. 0.f 0.0D .0 .0f .123D'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-number-literal'}] })),
// numbers - not decimal
{ src: '0.a', r: [{value: '0.', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
{ src: '0.0a', r: [{value: '0.0', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
// numbers - hex
...'0x0 0x123456789abcdef 0xABCDEF 0xabcdefl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
// numbers - partial hex
...'0x 0xl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
// numbers - decimal
...'0 123456789 0l'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'int-number-literal'}] })),
// strings
...[`"abc"`, `"\\n"`, `"\\""`].map(num => ({ src: num, r: [{value: num, kind:'string-literal'}] })),
// unterminated strings
...[`"abc`, `"\\n`, `"\\"`, `"`].map(num => ({ src: num, r: [{value: num, kind:'unterminated-string-literal'}] })),
// strings cannot cross newlines
{ src: `"abc\n`, r: [{value: `"abc`, kind:'unterminated-string-literal'}, {value: '\n', kind:'wsc'}] },
// characters
...[`'a'`, `'\\n'`, `'\\''`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
// unterminated/invalid characters
...[`'a`, `'\\n`, `'\\'`, `''`, `'`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
// characters cannot cross newlines
{ src: `'\n`, r: [{value: `'`, kind:'char-literal'}, {value: '\n', kind:'wsc'}] },
// arity symbol
{ src: `int...x`, r: [
{value: `int`, kind:'primitive-type'},
{value: `...`, kind:'symbol'},
{value: `x`, kind:'ident'},
],},
// complex inc - the javac compiler doesn't bother to try and sensibly separate +++ - it just appears to
// prioritise ++ in every case, assuming that the developer will insert spaces as required.
// e.g this first one fails to compile with javac
{ src: '++abc+++def', r: [
{value: '++', kind:'inc-operator'},
{value: 'abc', kind:'ident'},
{value: '++', kind:'inc-operator'},
{value: '+', kind:'plumin-operator'},
{value: 'def', kind:'ident'},
] },
// this should be ok
{ src: '++abc+ ++def', r: [
{value: '++', kind:'inc-operator'},
{value: 'abc', kind:'ident'},
{value: '+', kind:'plumin-operator'},
{value: ' ', kind:'wsc'},
{value: '++', kind:'inc-operator'},
{value: 'def', kind:'ident'},
] },
]
const report = (test, msg) => {
console.log(JSON.stringify({test, msg}));
}
tests.forEach(t => {
const tokens = tokenize(t.src);
if (tokens.length !== t.r.length) {
report(t, `Wrong token count. Expected ${t.r.length}, got ${tokens.length}`);
return;
}
for (let i=0; i < tokens.length; i++) {
if (tokens[i].value !== t.r[i].value)
report(t, `Wrong token value. Expected ${t.r[i].value}, got ${tokens[i].value}`);
if (tokens[i].kind !== t.r[i].kind)
report(t, `Wrong token kind. Expected ${t.r[i].kind}, got ${tokens[i].kind}`);
}
})
}
testTokenize();
// const s = require('fs').readFileSync('/home/dave/dev/vscode/android-dev-ext/langserver/tests/java-files/View-25.java', 'utf8');
// console.time();
// const tokens = tokenize(s);
// console.timeEnd();
// if (tokens.map(t => t.value).join('') !== s) {
// console.log('mismatch');
// }
// testTokenize();
exports.parseBody = parseBody;