From b050f3a82d776e02a25d036ba933afc05ff08787 Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Tue, 16 Jun 2020 12:59:08 +0100 Subject: [PATCH] start separating validation from parsing --- langserver/java/TokenList.js | 19 +- langserver/java/body-parser3.js | 1220 +++++------------------------- langserver/java/body-types.js | 13 +- langserver/java/source-types2.js | 4 +- 4 files changed, 217 insertions(+), 1039 deletions(-) diff --git a/langserver/java/TokenList.js b/langserver/java/TokenList.js index f8ce124..b4f7aef 100644 --- a/langserver/java/TokenList.js +++ b/langserver/java/TokenList.js @@ -57,6 +57,19 @@ class TokenList { return token; } + /** + * Check if the current token matches the specified kind, returns and consumes it + * @param {string} kind + */ + getIfKind(kind) { + const token = this.current; + if (token && token.kind === kind) { + this.inc(); + return token; + } + return null; + } + /** * Check if the current token matches the specified value, returns and consumes it * @param {string} value @@ -83,11 +96,7 @@ class TokenList { * @param {string} kind */ isKind(kind) { - if (this.current && this.current.kind === kind) { - this.inc(); - return true; - } - return false; + return this.getIfKind(kind) !== null; } /** diff --git a/langserver/java/body-parser3.js b/langserver/java/body-parser3.js index fa9023e..32a466d 100644 --- a/langserver/java/body-parser3.js +++ b/langserver/java/body-parser3.js @@ -4,18 +4,17 @@ * * Each token also contains detailed state information used for completion suggestions. */ -const { JavaType, CEIType, PrimitiveType, ArrayType, UnresolvedType, NullType, WildcardType, TypeVariableType, - TypeVariable, InferredTypeArgument, Field, Method, ReifiedMethod, Parameter, Constructor, signatureToType } = require('java-mti'); +const { JavaType, CEIType, PrimitiveType, ArrayType, UnresolvedType, NullType, TypeVariable } = require('java-mti'); const { SourceType, SourceTypeIdent, SourceField, SourceMethod, SourceConstructor, SourceInitialiser, SourceParameter, SourceAnnotation, SourceUnit, SourcePackage, SourceImport } = require('./source-types2'); const ResolvedImport = require('./parsetypes/resolved-import'); const ParseProblem = require('./parsetypes/parse-problem'); -const { getOperatorType, tokenize, Token } = require('./tokenizer'); +const { tokenize, Token } = require('./tokenizer'); const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver'); const { genericTypeArgs, typeIdent, typeIdentList } = require('./typeident'); const { TokenList } = require("./TokenList"); -const { AnyMethod, AnyType, AnyValue, ArrayElement, ArrayLiteral, ConstructorCall, Label, LiteralNumber, LiteralValue, Local, - MethodCall, MethodDeclarations, ResolvedIdent, TernaryValue, Value } = require("./body-types"); +const { AnyMethod, AnyType, AnyValue, ArrayLiteral, Label, LiteralNumber, LiteralValue, Local, + MethodDeclarations, ResolvedIdent, Value, ValueBase } = require("./body-types"); const { resolveImports, resolveSingleImport } = require('../java/import-resolver'); /** @@ -476,6 +475,128 @@ class AssertStatement extends Statement { message = null; } +class Expression extends ValueBase {} +class BinaryOpExpression extends Expression { + /** + * @param {ResolvedIdent} lhs + * @param {Token} op + * @param {ResolvedIdent} rhs + */ + constructor(lhs, op, rhs) { + super(); + this.lhs = lhs; + this.op = op; + this.rhs = rhs; + } +} +class TernaryOpExpression extends Expression { + /** + * @param {ResolvedIdent} test + * @param {ResolvedIdent} truthExpression + * @param {ResolvedIdent} falseExpression + */ + constructor(test, truthExpression, falseExpression) { + super(); + this.test = test; + this.truthExpression = truthExpression; + this.falseExpression = falseExpression; + } +} +class CastExpression extends Expression { + /** + * @param {ResolvedIdent} castType + * @param {ResolvedIdent} expression + */ + constructor(castType, expression) { + super(); + this.castType = castType; + this.expression = expression; + } +} +class BracketedExpression extends Expression { + /** + * @param {ResolvedIdent} expression + */ + constructor(expression) { + super(); + this.expression = expression; + } +} +class IncDecExpression extends Expression { + /** + * @param {ResolvedIdent} expr + * @param {Token} operator + * @param {'prefix'|'postfix'} which + */ + constructor(expr, operator, which) { + super(); + this.expr = expr; + this.operator = operator; + this.which = which; + } +} +class MethodCallExpression extends Expression { + /** + * @param {ResolvedIdent} instance + * @param {ResolvedIdent[]} args + */ + constructor(instance, args) { + super(); + this.instance = instance; + this.args = args; + } + +} + +class MemberExpression extends Expression { + /** + * @param {ResolvedIdent} instance + * @param {Token|null} member + */ + constructor(instance, member) { + super(); + this.instance = instance; + // member will be null for incomplete expressions + this.member = member; + } +} + +class ThisMemberExpression extends Expression { + /** + * @param {ResolvedIdent} instance + * @param {Token} this_token + */ + constructor(instance, this_token) { + super(); + this.instance = instance; + this.thisToken = this_token; + } +} + +class ClassMemberExpression extends Expression { + /** + * @param {ResolvedIdent} instance + * @param {Token} class_token + */ + constructor(instance, class_token) { + super(); + this.instance = instance; + this.classToken = class_token; + } +} + +class ArrayIndexExpression extends Expression { + /** + * @param {ResolvedIdent} instance + * @param {ResolvedIdent} index + */ + constructor(instance, index) { + super(); + this.instance = instance; + this.index = index; + } +} + /** * @param {Token[]} modifiers * @param {TokenList} tokens @@ -599,7 +720,7 @@ function fmc(modifiers, annotations, type_vars, type, tokens, imports, typemap) addproblem(tokens, ParseProblem.Error(tokens.current, `Fields cannot declare type variables`)); } const locals = var_ident_list(modifiers, decl_type_ident, name, tokens, new MethodDeclarations(), type, imports, typemap); - const fields = locals.map(l => new SourceField(type, modifiers, l.typeIdent, l.decltoken)); + const fields = locals.map(l => new SourceField(type, modifiers, l.typeIdent, l.decltoken, l.init)); type.fields.push(...fields); } semicolon(tokens); @@ -906,13 +1027,6 @@ function statementKeyword(tokens, mdecls, method, imports, typemap) { tokens.inc(); s = new ReturnStatement(); s.expression = isExpressionStart(tokens.current) ? expression(tokens, mdecls, 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': @@ -920,7 +1034,6 @@ function statementKeyword(tokens, mdecls, method, imports, typemap) { s = new ThrowStatement(); if (!tokens.isValue(';')) { s.expression = isExpressionStart(tokens.current) ? expression(tokens, mdecls, method, imports, typemap) : null; - checkThrowExpression(tokens, s.expression, typemap); semicolon(tokens); } break; @@ -961,9 +1074,6 @@ function statementKeyword(tokens, mdecls, method, imports, typemap) { function bracketedTest(tokens, mdecls, scope, imports, typemap) { tokens.expectValue('('); const e = expression(tokens, mdecls, scope, 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; } @@ -1040,23 +1150,6 @@ function enhancedFor(s, tokens, mdecls, method, imports, typemap) { 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, mdecls, method, imports, typemap); } @@ -1072,11 +1165,6 @@ function enhancedFor(s, tokens, mdecls, method, imports, typemap) { function synchronizedStatement(s, tokens, mdecls, method, imports, typemap) { tokens.expectValue('('); s.expression = expression(tokens, mdecls, method, imports, typemap); - if (s.expression.variables[0]) { - if (s.expression.variables[0].type instanceof PrimitiveType) { - addproblem(tokens, ParseProblem.Error(tokens.current, `synchronized lock expression must be a reference type`)); - } - } tokens.expectValue(')'); s.statement = nonVarDeclStatement(tokens, mdecls, method, imports, typemap); } @@ -1091,15 +1179,8 @@ function synchronizedStatement(s, tokens, mdecls, method, imports, typemap) { */ function assertStatement(s, tokens, mdecls, method, imports, typemap) { s.expression = expression(tokens, mdecls, method, imports, typemap); - if (s.expression.variables[0] && !isAssignable(PrimitiveType.map.Z, s.expression.variables[0])) { - addproblem(tokens, ParseProblem.Error(tokens.current, `Boolean expression expected but type '${s.expression.variables[0].type.fullyDottedTypeName}' found`)); - } - if (tokens.isValue(':')) { s.message = expression(tokens, mdecls, method, imports, typemap); - if (s.message.variables[0] && (s.message.variables[0].type === PrimitiveType.map.V)) { - addproblem(tokens, ParseProblem.Error(tokens.current, `assert message expression cannot be void`)); - } } } @@ -1148,7 +1229,7 @@ function catchFinallyBlocks(s, tokens, mdecls, method, imports, typemap) { mdecls.pushScope(); let exceptionVar; if (catchinfo.types[0] && catchinfo.name) { - exceptionVar = new Local(mods, catchinfo.name.value, catchinfo.name, catchinfo.types[0], 0); + exceptionVar = new Local(mods, catchinfo.name.value, catchinfo.name, catchinfo.types[0], 0, null); mdecls.locals.push(exceptionVar); } catchinfo.block = statementBlock(tokens, mdecls, method, imports, typemap); @@ -1196,20 +1277,11 @@ function catchType(tokens, mdecls, method, imports, typemap) { function switchBlock(s, tokens, mdecls, method, imports, typemap) { tokens.expectValue('('); s.test = expression(tokens, mdecls, 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, mdecls, method, imports, typemap); + caseBlock(s, tokens, mdecls, method, imports, typemap); continue; } addproblem(tokens, ParseProblem.Error(tokens.current, 'case statement expected')); @@ -1220,16 +1292,15 @@ function switchBlock(s, tokens, mdecls, method, imports, typemap) { /** * @param {SwitchStatement} s -* @param {JavaType} test_type * @param {TokenList} tokens * @param {MethodDeclarations} mdecls * @param {SourceMC} method * @param {ResolvedImport[]} imports * @param {Map} typemap */ -function caseBlock(s, test_type, tokens, mdecls, method, imports, typemap) { +function caseBlock(s, tokens, mdecls, method, imports, typemap) { const case_start_idx = s.cases.length; - caseExpressionList(s.cases, test_type, tokens, mdecls, method, imports, typemap); + caseExpressionList(s.cases, tokens, mdecls, method, imports, typemap); const statements = []; for (;;) { if (/^(case|default|\})$/.test(tokens.current.value)) { @@ -1246,34 +1317,32 @@ function caseBlock(s, test_type, tokens, mdecls, method, imports, typemap) { /** * @param {(ResolvedIdent|boolean)[]} cases -* @param {JavaType} test_type * @param {TokenList} tokens * @param {MethodDeclarations} mdecls * @param {SourceMC} method * @param {ResolvedImport[]} imports * @param {Map} typemap */ -function caseExpressionList(cases, test_type, tokens, mdecls, method, imports, typemap) { - let c = caseExpression(cases, test_type, tokens, mdecls, method, imports, typemap); +function caseExpressionList(cases, tokens, mdecls, method, imports, typemap) { + let c = caseExpression(cases, tokens, mdecls, method, imports, typemap); if (!c) { return; } while (c) { cases.push(c); - c = caseExpression(cases, test_type, tokens, mdecls, method, imports, typemap); + c = caseExpression(cases, tokens, mdecls, method, imports, typemap); } } /** * @param {(ResolvedIdent|boolean)[]} cases -* @param {JavaType} test_type * @param {TokenList} tokens * @param {MethodDeclarations} mdecls * @param {SourceMC} method * @param {ResolvedImport[]} imports * @param {Map} typemap */ -function caseExpression(cases, test_type, tokens, mdecls, method, imports, typemap) { +function caseExpression(cases, tokens, mdecls, method, imports, typemap) { /** @type {boolean|ResolvedIdent} */ let e = tokens.isValue('default'); if (e && cases.find(c => c === e)) { @@ -1282,15 +1351,6 @@ function caseExpression(cases, test_type, tokens, mdecls, method, imports, typem if (!e) { if (tokens.isValue('case')) { e = expression(tokens, mdecls, 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) { @@ -1299,66 +1359,6 @@ function caseExpression(cases, test_type, tokens, mdecls, method, imports, typem 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} 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 {Token[]} mods @@ -1395,10 +1395,7 @@ function var_ident_list(mods, type, first_ident, tokens, mdecls, scope, imports, } // only add the local if we have a name if (name) { - const local = new Local(mods, name.value, name, type, postnamearrdims); - local.init = init; - if (init && init.variables[0]) - checkAssignmentExpression(tokens, local, op, init.variables[0]); + const local = new Local(mods, name.value, name, type, postnamearrdims, init); new_locals.push(local); } if (tokens.isValue(',')) { @@ -1503,12 +1500,11 @@ function expression(tokens, mdecls, scope, imports, typemap, precedence_stack = const rhs = expression(tokens, mdecls, scope, imports, typemap, [operator_precedence, ...precedence_stack]); if (binary_operator.value === '?') { - const colon = tokens.current; tokens.expectValue(':'); const falseStatement = expression(tokens, mdecls, scope, imports, typemap, [operator_precedence, ...precedence_stack]); - matches = resolveTernaryExpression(tokens, matches, colon, rhs, falseStatement); + matches = new ResolvedIdent(`${matches.source} ? ${rhs.source} : ${falseStatement.source}`, [new TernaryOpExpression(matches, rhs, falseStatement)]); } else { - matches = resolveBinaryOpExpression(tokens, matches, binary_operator, rhs); + matches = new ResolvedIdent(`${matches.source} ${binary_operator.value} ${rhs.source}`, [new BinaryOpExpression(matches, binary_operator, rhs)]); } } @@ -1516,636 +1512,6 @@ function expression(tokens, mdecls, scope, imports, typemap, precedence_stack = 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; - } - const is_assignable = isAssignable(variable.type, value); - if (!is_assignable) { - if (value instanceof ArrayLiteral) { - addproblem(tokens, ParseProblem.Error(op, `Array literal expression is not compatible with variable of type '${variable.type.fullyDottedTypeName}'`)); - } else { - 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} arr_literal_value - */ -function isArrayAssignable(variable_type, arr_literal_value) { - if (!(variable_type instanceof ArrayType)) { - return false; - } - // empty array literals are compatible with all arrays - if (arr_literal_value.elements.length === 0) { - return true; - } - const required_element_type = variable_type.elementType; - for (let i=0; i < arr_literal_value.elements.length; i++) { - const element_value = arr_literal_value.elements[i].variables[0]; - let is_assignable = !!element_value && isAssignable(required_element_type, element_value); - 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); - } - if (value instanceof ArrayLiteral) { - return isArrayAssignable(type, value); - } - - 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 NullType) { - // null is castable to any non-primitive - return !(cast_type instanceof PrimitiveType); - } - 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': - case 'C': - 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_types = { - // conversions from a primitive to a value - from: { - B: /^[BSIJFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double);$/, - S: /^[SIJFD]$|^Ljava\/lang\/(Short|Integer|Long|Float|Double);$/, - I: /^[IJFD]$|^Ljava\/lang\/(Integer|Long|Float|Double);$/, - J: /^[JFD]$|^Ljava\/lang\/(Long|Float|Double);$/, - F: /^[FD]$|^Ljava\/lang\/(Float|Double);$/, - D: /^D$|^Ljava\/lang\/(Double);$/, - C: /^[CIJFD]$|^Ljava\/lang\/(Character|Integer|Long|Float|Double);$/, - Z: /^Z$|^Ljava\/lang\/(Boolean);$/, - V: /$^/, // V.test() always returns false - }, - // conversions to a primitive from a value - to: { - B: /^[B]$|^Ljava\/lang\/(Byte);$/, - S: /^[BS]$|^Ljava\/lang\/(Byte|Short);$/, - I: /^[BSIC]$|^Ljava\/lang\/(Byte|Short|Integer|Character);$/, - J: /^[BSIJC]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character);$/, - F: /^[BSIJCF]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float);$/, - D: /^[BSIJCFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|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) { - // primitive values can only be assigned to wider primitives or their class equivilents - is_assignable = valid_primitive_types.from[value_type.typeSignature].test(dest_type.typeSignature); - } else if (dest_type instanceof PrimitiveType) { - // primitive variables can only be assigned from narrower primitives or their class equivilents - is_assignable = valid_primitive_types.to[dest_type.typeSignature].test(value_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); - if (!is_assignable) { - // generic types are also assignable to compatible wildcard type bounds - const raw_type = valid_raw_types.find(rt => rt.rawTypeSignature === dest_type.rawTypeSignature); - if (raw_type instanceof CEIType && raw_type.typeVariables.length === value_type.typeVariables.length) { - is_assignable = dest_type.typeVariables.every((dest_tv, idx) => isTypeArgumentCompatible(dest_tv, value_type.typeVariables[idx].type)); - } - } - } - } - return is_assignable; -} - -/** - * @param {TypeVariable} dest_typevar - * @param {JavaType} value_typevar_type - */ -function isTypeArgumentCompatible(dest_typevar, value_typevar_type) { - if (dest_typevar.type instanceof WildcardType) { - if (!dest_typevar.type.bound) { - // unbounded wildcard types are compatible with everything - return true; - } - if (dest_typevar.type.bound.type === value_typevar_type) { - return true; - } - switch (dest_typevar.type.bound.kind) { - case 'extends': - return isTypeAssignable(dest_typevar.type.bound.type, value_typevar_type); - case 'super':; - return isTypeAssignable(value_typevar_type, dest_typevar.type.bound.type); - } - return false; - } - if (value_typevar_type instanceof TypeVariableType) { - // inferred type arguments of the form `x = List<>` are compatible with every destination type variable - return value_typevar_type.typeVariable instanceof InferredTypeArgument; - } - return dest_typevar.type === value_typevar_type; -} - -/** - * @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 {JavaType} lhs_type - * @param {JavaType} rhs_type - */ -function isTypeComparable(lhs_type, rhs_type) { - 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) { - if (lhs_type.rawTypeSignature === rhs_type.rawTypeSignature) { - is_comparable = lhs_type.typeVariables.every((tv, idx) => isTypeArgumentComparable(tv, rhs_type.typeVariables[idx])); - } - } - } - return is_comparable; -} - -/** - * @param {TypeVariable} a - * @param {TypeVariable} b - */ -function isTypeArgumentComparable(a, b) { - let a_type = a.type, b_type = b.type; - if (a_type === b_type) { - return true; - } - if (a_type instanceof WildcardType) { - if (!a_type.bound) - return true; // unbounded wildcard types are comparable with everything - if (a_type.bound.type.typeKind === 'interface') - return true; // interface bounds are comparable with everything - } - if (b_type instanceof WildcardType) { - if (!b_type.bound) - return true; // unbounded wildcard types are comparable with everything - if (b_type.bound.type.typeKind === 'interface') - return true; // interface bounds are comparable with everything - } - /** - * - * @param {JavaType} type - * @param {JavaType} list_type - */ - function extendsFrom(type, list_type) { - if (!(list_type instanceof CEIType)) { - return false; - } - return list_type === type || getTypeInheritanceList(list_type).includes(type); - } - // each type argument can have 3 possible states - // - a extends, a super, a (exact) - // - b extends, b super, b (exact) - // we need to cover all combinations of a and b... - if (a_type instanceof WildcardType && a_type.bound.kind === 'extends') { - if (b_type instanceof WildcardType && b_type.bound.kind === 'extends') { - // both are extends - one must extend from the other - return extendsFrom(a_type.bound.type, b_type.bound.type) || extendsFrom(b_type.bound.type, a_type.bound.type); - } - else if (b_type instanceof WildcardType && b_type.bound.kind === 'super') { - // a extends, b super - b must extend from a - return extendsFrom(a_type.bound.type, b_type.bound.type); - } else { - // b is an exact type - b must extend from a - return extendsFrom(a_type.bound.type, b_type); - } - } - else if (a_type instanceof WildcardType && a_type.bound.kind === 'super') { - if (b_type instanceof WildcardType && b_type.bound.kind === 'super') { - // both are super - one must extend from the other - return extendsFrom(a_type.bound.type, b_type.bound.type) || extendsFrom(b_type.bound.type, a_type.bound.type); - } - else if (b_type instanceof WildcardType && b_type.bound.kind === 'extends') { - // a super, b extends - a must extend from b - return extendsFrom(b_type.bound.type, a_type.bound.type); - } else { - // b is an exact type - a must extend from b - return extendsFrom(b_type, a_type.bound.type); - } - } else { - // a is an exact type - if (b_type instanceof WildcardType && b_type.bound.kind === 'extends') { - // a exact, b extends - a must extend from b - return extendsFrom(b_type.bound.type, a_type); - } - else if (b_type instanceof WildcardType && b_type.bound.kind === 'super') { - // a exact, b super - b must extend from a - return extendsFrom(a_type, b_type.bound.type); - } - } - return false; -} - -/** - * @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) { - const is_comparable = isTypeComparable(lhs.type, rhs.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 {MethodDeclarations} mdecls @@ -2155,15 +1521,9 @@ function resolveMath(tokens, ident, lhs, op, rhs) { */ function qualifiedTerm(tokens, mdecls, scope, imports, typemap) { let matches = rootTerm(tokens, mdecls, scope, 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); + const postfix_operator = tokens.getIfKind('inc-operator'); + if (postfix_operator) { + return new ResolvedIdent(`${matches.source}${postfix_operator.value}`, [new IncDecExpression(matches, postfix_operator, 'postfix')]); } matches = qualifiers(matches, tokens, mdecls, scope, imports, typemap); return matches; @@ -2248,17 +1608,9 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { matches = new ResolvedIdent(tokens.current.value, [LiteralNumber.from(tokens.current)]); break; case 'inc-operator': - let incop = tokens.current; - tokens.inc(); + let incop = tokens.getIfKind('inc-operator'); matches = qualifiedTerm(tokens, mdecls, scope, 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)]); + return new ResolvedIdent(`${incop.value}${matches.source}`, [new IncDecExpression(matches, incop, 'prefix')]) case 'plumin-operator': case 'unary-operator': tokens.inc(); @@ -2268,28 +1620,15 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { case 'open-bracket': tokens.inc(); matches = expression(tokens, mdecls, scope, 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, mdecls, scope, 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); + const expression = qualifiedTerm(tokens, mdecls, scope, imports, typemap) + return new ResolvedIdent(`(${matches.source})${expression.source}`, [new CastExpression(matches, expression)]); } // 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); + return new ResolvedIdent(`(${matches.source})`, [new BracketedExpression(matches)]); case tokens.isValue('{') && 'symbol': // array initer let elements = []; @@ -2387,152 +1726,15 @@ function expressionList(tokens, mdecls, scope, imports, typemap, opts) { return expressions; } -/** - * @param {TokenList} tokens - * @param {MethodDeclarations} mdecls - * @param {Scope} scope - * @param {ResolvedImport[]} imports - * @param {Map} typemap - */ -function arrayIndexOrDimension(tokens, mdecls, scope, imports, typemap) { - let e = expression(tokens, mdecls, scope, 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} instance * @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 => new ArrayType(t, 1)); - - if (!variables[0] && !types[0]) { - addproblem(tokens, ParseProblem.Error(open_array, `Invalid array expression: '${matches.source}' is not an array type`)); - variables.push(new ArrayElement(new Value(matches.source, new ArrayType(AnyType.Instance, 1)), index)); - } - return new ResolvedIdent(ident, variables, [], types); -} - -/** - * @param {TokenList} tokens - * @param {ResolvedIdent} instance - * @param {ResolvedIdent[]} call_arguments - * @param {Map} 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.forEach(m => { - if (m.typeVariables.length) { - // if the method is declared with type variables, specialise it based upon the argument types - m = ReifiedMethod.build(m, call_arguments.map(arg => arg.variables[0].type)); - } - if (isCallCompatible(m, call_arguments)) { - methods.push(m); - } - }); - 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 : '').join(', '); - if (instance.methods[0]) { - if (!(instance.methods[0] instanceof AnyMethod)) { - 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]) { - if (!(instance.types[0] instanceof AnyType)) { - 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; - } - const param_count = m.parameterCount; - if (param_count !== call_arguments.length) { - // for variable arity methods, we must have at least n-1 formal parameters - if (!m.isVariableArity || call_arguments.length < param_count - 1) { - // wrong parameter count - return false; - } - } - const formal_params = m.parameters.slice(); - const last_param = formal_params.pop(); - for (let i = 0; i < call_arguments.length; i++) { - if (!call_arguments[i].variables[0]) { - // only variables can be passed - not types or methods - return false; - } - const param = formal_params[i] || last_param; - let param_type = param.type; - if (param.varargs && param_type instanceof ArrayType) { - // last varargs parameter - // - if the argument count matches the parameter count, the final argument can match the array or non-array version - // e.g void v(int... x) will match with v(), v(1) and v(new int[3]); - if (call_arguments.length === param_count) { - if (isAssignable(param_type, call_arguments[i].variables[0])) { - continue; - } - } - param_type = param_type.elementType; - } - // is the argument assignable to the parameter - if (isAssignable(param_type, call_arguments[i].variables[0])) { - continue; - } - // mismatch parameter type - return false; - } - return true; +function arrayElementOrConstructor(tokens, open_array, instance, index) { + const ident = `${instance.source}[${index.source}]`; + return new ResolvedIdent(ident, [new ArrayIndexExpression(instance, index)]); } /** @@ -2569,7 +1771,7 @@ function qualifiers(matches, tokens, mdecls, scope, imports, typemap) { for (;;) { switch (tokens.current.value) { case '.': - matches = dottedIdent(matches, tokens, typemap); + matches = memberQualifier(matches, tokens, mdecls, scope, imports, typemap); break; case '[': matches = arrayQualifiers(matches, tokens, mdecls, scope, imports, typemap); @@ -2580,8 +1782,8 @@ function qualifiers(matches, tokens, mdecls, scope, imports, 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]) { + // it if there is at least one type + if (!matches.types[0]) { return matches; } tokens.inc(); @@ -2593,6 +1795,42 @@ function qualifiers(matches, tokens, mdecls, scope, imports, typemap) { } } +/** + * @param {ResolvedIdent} matches + * @param {TokenList} tokens + * @param {MethodDeclarations} mdecls + * @param {Scope} scope + * @param {ResolvedImport[]} imports + * @param {Map} typemap + */ +function memberQualifier(matches, tokens, mdecls, scope, imports, typemap) { + tokens.expectValue('.'); + let expr, label = `${matches.source}.${tokens.current.value}`; + let types = [], package_name = ''; + switch (tokens.current.value) { + case 'class': + expr = new ClassMemberExpression(matches, tokens.current); + break; + case 'this': + expr = new ThisMemberExpression(matches, tokens.current); + break; + default: + let member = tokens.current; + if (member.kind === 'ident') { + if (matches.package_name || matches.types[0]) { + ({ types, package_name } = resolveNextTypeOrPackage(member.value, matches.types, matches.package_name, typemap)); + } + } else { + member = null; + addproblem(tokens, ParseProblem.Error(tokens.current, `Identifier expected`)); + } + expr = new MemberExpression(matches, member); + break; + } + tokens.inc(); + return new ResolvedIdent(label, [expr], [], types, package_name); +} + /** * @param {ResolvedIdent} matches * @param {TokenList} tokens @@ -2609,7 +1847,7 @@ function arrayQualifiers(matches, tokens, mdecls, scope, imports, typemap) { matches = arrayTypeExpression(matches); } else { // array index - const index = arrayIndexOrDimension(tokens, mdecls, scope, imports, typemap); + const index = expression(tokens, mdecls, scope, imports, typemap); matches = arrayElementOrConstructor(tokens, open_array, matches, index); // @ts-ignore tokens.expectValue(']'); @@ -2633,7 +1871,7 @@ function methodCallQualifier(matches, tokens, mdecls, scope, imports, typemap) { args = expressionList(tokens, mdecls, scope, imports, typemap); tokens.expectValue(')'); } - return methodCallExpression(tokens, matches, args, typemap); + return new ResolvedIdent(`${matches.source}(${args.map(a => a.source).join(', ')})`, [new MethodCallExpression(matches, args)]); } /** @@ -2644,82 +1882,6 @@ function arrayTypeExpression(matches) { return new ResolvedIdent(`${matches.source}[]`, [], [], types); } -/** - * - * @param {ResolvedIdent} matches - * @param {TokenList} tokens - * @param {Map} typemap - */ -function dottedIdent(matches, tokens, typemap) { - tokens.expectValue('.'); - 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(); - const prim_map = { - B:'Byte',S:'Short',I:'Integer',J:'Long',F:'Float',D:'Double',C:'Character',Z:'Boolean',V:'Void', - } - variables = matches.types.map(t => { - const type_signature = t instanceof AnyType - ? '' - : t instanceof PrimitiveType - ? `` - : `<${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 will 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; - } - // search static fields and methods - const decls = t.findDeclsByName(tokens.current.value); - variables.push(...decls.fields); - methods.push(...decls.methods); - }); - - const members = resolveNextTypeOrPackage(tokens.current.value, matches.types, matches.package_name, typemap); - - const match = new ResolvedIdent(qualified_ident, variables, methods, [...types, ...members.types ], members.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. diff --git a/langserver/java/body-types.js b/langserver/java/body-types.js index 21f02de..c04fa60 100644 --- a/langserver/java/body-types.js +++ b/langserver/java/body-types.js @@ -4,7 +4,7 @@ const { Token } = require('./tokenizer'); class ResolvedIdent { /** * @param {string} ident - * @param {(Local|Parameter|Field|ArrayElement|Value)[]} variables + * @param {(Local|Parameter|Field|ArrayElement|ValueBase)[]} variables * @param {Method[]} methods * @param {JavaType[]} types * @param {string} package_name @@ -66,8 +66,9 @@ class Local { * @param {Token} decltoken * @param {import('./source-type').SourceTypeIdent} typeIdent * @param {number} postnamearrdims + * @param {ResolvedIdent} init */ - constructor(modifiers, name, decltoken, typeIdent, postnamearrdims) { + constructor(modifiers, name, decltoken, typeIdent, postnamearrdims, init) { this.finalToken = modifiers.find(m => m.source === 'final') || null; this.name = name; this.decltoken = decltoken; @@ -75,7 +76,7 @@ class Local { typeIdent.resolved = new ArrayType(typeIdent.resolved, postnamearrdims); } this.typeIdent = typeIdent; - this.init = null; + this.init = init; } get type() { @@ -132,12 +133,15 @@ class ArrayElement { } } -class Value { +class ValueBase {} + +class Value extends ValueBase { /** * @param {string} name * @param {JavaType} type */ constructor(name, type) { + super(); this.name = name; this.type = type; } @@ -389,3 +393,4 @@ exports.MethodDeclarations = MethodDeclarations; exports.ResolvedIdent = ResolvedIdent; exports.TernaryValue = TernaryValue; exports.Value = Value; +exports.ValueBase = ValueBase; diff --git a/langserver/java/source-types2.js b/langserver/java/source-types2.js index cbef1bd..a6690c1 100644 --- a/langserver/java/source-types2.js +++ b/langserver/java/source-types2.js @@ -106,13 +106,15 @@ class SourceField extends Field { * @param {Token[]} modifiers * @param {SourceTypeIdent} field_type_ident * @param {Token} name_token + * @param {ResolvedIdent} init */ - constructor(owner, modifiers, field_type_ident, name_token) { + constructor(owner, modifiers, field_type_ident, name_token, init) { super(modifiers.map(m => m.value), ''); this.owner = owner; this.modifierTokens = modifiers; this.fieldTypeIdent = field_type_ident; this.nameToken = name_token; + this.init = init; } get name() {