mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
version 1.2 (#93)
* initial working language server * first hacky version of source parsing and type checking * first iteration of method body parser * add support for prefix/postfix inc expressions * add basic support for parsing new expressions * different attempt to parse using collapsable text ranges * fix parsing of binary operstors following a bracket expression * updated validation to use new JavaTypes module instead of MTIs * add support for array-literal expressions * fix || and && not being tokenized as operators allow float literals starting with dot * add new method body parser to use direct linear parsing * add super as an object literal * fix interface constructors check constructor type modifiers * fix assignment operator types * Fix resolving of enclosed type identifiers * add default constructor for class types with no explicit constructors * add missing constructor validator * add constructor parameters to list of resolvable types * update SourceMethod to pass name in super constructor * add Any* classes to reduce cascading errors * update method call parameter checking use isTypeAssignable instead of getParameterCompatibleTypeSignatures * tidy up isTypeAssignable allow class equivilents for primitives * add more info when methods/ctrs cannot be matched * allow interfaces to be cast to class instances * use isTypeAssignable for checking branch test expressions * allow AnyValue to be a constant value * split shift operators from bitwise operators * add support for literal numbers to be assignable to multiple primtive types * clear diagnostics when document is closed * update check for cast expression * casting only applies to qualified term not a whole expression * allow all primitive-number-type casts * add support for synchronized statement * update primitive type compatibility * allow null to be cast to any non-primitve * use better regex for string literals * allow character literals to be assigned to number types * add support for array qualifiers after a variable name * make sure any long specifier is stripped from a bigint value * improve invalid array expression message add AnyType array element to prevent cascading errors * make default a modifer keyword for interface default method support * initial support for wildcard type arguments * fix parse issue with nested generic types * allow generic types to be assigned to inherited types with compatible type arguments * allow unicode characters, $ and _ in identifiers * map primitive types to their boxed versions for class member * support assert statement * allow unicode char literals * make type parser and body parser use same tokenizer * reuse parsed tokens instead of tokenizing each method body * re-add throws as a keyword * treat default and synchronized as modifiers * add SourceInitialiser support * refactor to prepare for merging with type parsing * add support for array qualifiers in type identifiers * pass scoped type instead of method to typeIdent * update ResolvableType to use same type resolving as method body parsing * add support for post-name array qualifiers in fields and parameters * post-name array qualifiers in method decls * add type variables to SourceMethod * initial attempt to support type variable arguments in methods * specialise methods with type variables * don't require default interface methods to be implemented * make variable arity parameters an array type * tidy array constructors and fix some warnings * update isCallCompatible to handle variable arity calls * improve assert statement support * parse labels and break/continue targets * refactor new term qualifiers * add support for generic inferred-type arguments * improve modifier checks for interface types * improve reporting of unresolved type errors * fix type checking of field and method declarations * add missng strictfp modifier * refactor in preparation for parsing local types * replace Locals with scopeable MethodDeclarations to allow labels and types to be stored * initial changes to support local type declarations * update to use new set of SourceX classes * refactor to allow expressions to have a type scope * replace regex parsing with linear parsing * generate source types before parsing * fix support for resolving type variables in method declarations * fix checking of array literal compatability * report errors from unit parsing * remove local modifier validation during parse add parameter modifier checking to validation * allow trailing comma for array literals * start separating validation from parsing * add support for parsing enum values * allow uppercase 0X in hex literals * include enclosing types in identifier search * add support for parsing parameterless lambdas * ignore unresolved types in extends/implements * implement specialisation of SourceType * allow super as a member qualifier * allow empty enums * don't report missing constructors if superclass has none * update typemap declarations to use CEIType instead of JavaType * fix resolving of class type variables * fix bad imports when resolving annotations * allow null scope in findIdentifier * add support for static member imports * import types from same package * remove this qualifier from isCastExpression * add hex exponent support * parse try-with-resources * fix resolving imported enclosed types * extract expression types into separate files * extract statement types into separate files * fix type warnings * extract literals into separate files * remove Value class, add NewExpression and separate out Any classes * rename source types module * remove some parse checks that should be in verify * support token extraction in expressions * implement resolveExpression * add type cast checking * check for valid type in class member expressions * allow assigns for assignable type arguments * improve reporting of unresolved identifiers * add new array validation * validate array literals * validate array indexes * improve validation of binary operators * rename ResolvedType to ResolvedValue * improve checking of number literals * support package name as a resolved value * implement method body and ststement validation * improve method call resolving * add support for this() and super() constructor calls * remove return type for source constructors * add checks for unary operators * ensure tokens are assigned for qualified expressions * check castability using type assignments * add implicit enum methods values() and valueOf() * add basic type checking of lambda expressions * fix return type check * fix assert statement checks * improve support for ternary operators in assignments and method invocations * perform more detailed search of implemented methods * initial test of context-dependant code completion * support package, type and static field import completion * support for member expressions * use exact type signatures for locating types for completion items * add support for field and method docs * add support for docs in source types * support member completion for array types improve comment formatting * ensure Object is always last in the list of inherited types * add owning method to statements create common keyword statement class * improve code completion list add method parameters order list items by scope * add source types to list hide this and super for non-methods * fix bad member resolution at end of block fix missing method and type docs * add support for editing multiple files * allow multiple source files to be used in parsing * load and parse files at startup * add support for displaying method signatures * add single trace function with timestamps * implement shceduleReparse to reduce parsing load while typing * remove parsed type list logging * wait for reparsing before returning method signatures * resolve new object contructors * improve extraction of parameter docs * update @types/vscode * cache decoded android library in globalStoragePath * load single android library cache from local folder * android-29 library cache * allow configurable app root setting * set configurable trace logging and update section names * description updates * handle null token passed to ParseProblem * refactoring * Rename language client extension to Android * ignore unnamed type declarations * handle java file change notifications * make sure we only try and parse java files * add option to allow language server to be shutdown * simplify handling of this and class member qualifiers * relocate java-mti package into project * get main node install to install langserver dependencies * remove debugging pause * rename body-parser3 to body-parser * clean up import resolving code * remove unused field from ResolvedImport * remove validation modules that used old parser types * remove old parser files * remove redundant types and functions used by old parser * move addproblem into TokenList * remove unused ResolvedType class * validate more statements * add support for parsing and validating anonymous types * hide some method modifiers which aren't useful to show * code comments and minor improvements * fix some type warnings * improve support for completion of enum values * add type name to parameter completion labels * ignore synthetic members in completion list * use a specialised map for handling case-insenstive file uris * add basic build script * reference java-mti package from GitHub * revert @types/vscode * update initial file loading to use URIs passed from the client changes to the appSourceRoot now require an extension restart * add support for loading filtered androidx libraries for code completion * update version of java-mti * add mixpanel package * add basic analytics * fix dependency versions * fix dependency versions * set empty cache file markers * add language server debug config * add file to build script * add unqualified type members when inside a method * apply statics filter to enum values * add basic debugger analytics * include current time in startup event * add terminate reason to debugger * update changelog and readme
This commit is contained in:
175
langserver/java/TokenList.js
Normal file
175
langserver/java/TokenList.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
*/
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TokenList} tokens
|
||||
* @param {ParseProblem} problem
|
||||
*/
|
||||
function addproblem(tokens, problem) {
|
||||
tokens.problems.push(problem);
|
||||
}
|
||||
|
||||
class TokenList {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
constructor(tokens) {
|
||||
this.tokens = tokens;
|
||||
this.idx = -1;
|
||||
/** @type {Token} */
|
||||
this.current = null;
|
||||
this.inc();
|
||||
/** @type {ParseProblem[]} */
|
||||
this.problems = [];
|
||||
this.marks = [];
|
||||
this.last_mlc = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns and consumes the current token
|
||||
*/
|
||||
consume() {
|
||||
const tok = this.current;
|
||||
this.inc();
|
||||
return tok;
|
||||
}
|
||||
|
||||
inc() {
|
||||
for (; ;) {
|
||||
this.current = this.tokens[this.idx += 1];
|
||||
if (!this.current || this.current.kind !== 'wsc') {
|
||||
return this.current;
|
||||
}
|
||||
const wsc = this.current.value;
|
||||
if (wsc.startsWith('/*')) {
|
||||
this.last_mlc = wsc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearMLC() {
|
||||
this.last_mlc = '';
|
||||
}
|
||||
|
||||
getLastMLC() {
|
||||
const s = this.last_mlc;
|
||||
this.last_mlc = '';
|
||||
return s;
|
||||
}
|
||||
|
||||
mark() {
|
||||
this.marks.unshift(this.idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of tokens from the last mark() point, trimming any trailing whitespace tokens
|
||||
*/
|
||||
markEnd() {
|
||||
let i = this.idx;
|
||||
while (this.tokens[--i].kind === 'wsc') { }
|
||||
const range = [this.marks.shift(), i + 1];
|
||||
if (range[1] <= range[0]) {
|
||||
range[1] = range[0] + 1;
|
||||
}
|
||||
return this.tokens.slice(range[0], range[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token lookahead. The current token is unaffected by this method.
|
||||
* @param {number} n number of tokens to look ahead
|
||||
*/
|
||||
peek(n) {
|
||||
let token, idx = this.idx;
|
||||
while (--n >= 0) {
|
||||
for (; ;) {
|
||||
token = this.tokens[idx += 1];
|
||||
if (!token || token.kind !== 'wsc') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
*/
|
||||
getIfValue(value) {
|
||||
const token = this.current;
|
||||
if (token && token.value === value) {
|
||||
this.inc();
|
||||
return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified value and consumes it
|
||||
* @param {string} value
|
||||
*/
|
||||
isValue(value) {
|
||||
return this.getIfValue(value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified kind and consumes it
|
||||
* @param {string} kind
|
||||
*/
|
||||
isKind(kind) {
|
||||
return this.getIfKind(kind) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
const token = this.current || this.tokens[this.tokens.length - 1];
|
||||
addproblem(this, ParseProblem.Error(token, `${value} expected`));
|
||||
return false;
|
||||
}
|
||||
|
||||
get previous() {
|
||||
for (let idx = this.idx - 1; idx >= 0; idx--) {
|
||||
if (idx <= 0) {
|
||||
return this.tokens[0];
|
||||
}
|
||||
if (this.tokens[idx].kind !== 'wsc') {
|
||||
return this.tokens[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} delete_count
|
||||
* @param {...Token} insert
|
||||
*/
|
||||
splice(start, delete_count, ...insert) {
|
||||
this.tokens.splice(start, delete_count, ...insert);
|
||||
this.current = this.tokens[this.idx];
|
||||
}
|
||||
}
|
||||
|
||||
exports.TokenList = TokenList;
|
||||
exports.addproblem = addproblem;
|
||||
172
langserver/java/anys.js
Normal file
172
langserver/java/anys.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const { JavaType, Method } = require('java-mti');
|
||||
const { Expression } = require('./expressiontypes/Expression');
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom type designed to be used where a type is missing or unresolved.
|
||||
*
|
||||
* 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 || '<unknown type>';
|
||||
}
|
||||
|
||||
static Instance = new AnyType('');
|
||||
|
||||
get rawTypeSignature() {
|
||||
return 'U';
|
||||
}
|
||||
|
||||
get typeSignature() {
|
||||
return 'U';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom method designed to be compatible with
|
||||
* any arguments in method call
|
||||
*/
|
||||
class AnyMethod extends Method {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(null, name, [], '');
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom expression designed to be compatiable with
|
||||
* any variable or operator
|
||||
*/
|
||||
class AnyValue extends Expression {
|
||||
/**
|
||||
*
|
||||
* @param {String} label
|
||||
*/
|
||||
constructor(label) {
|
||||
super();
|
||||
this.label = label;
|
||||
this.type = AnyType.Instance;
|
||||
}
|
||||
|
||||
resolveExpression() {
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent a method identifier
|
||||
*
|
||||
* e.g `"".length`
|
||||
*/
|
||||
class MethodType {
|
||||
/**
|
||||
* @param {Method[]} methods
|
||||
*/
|
||||
constructor(methods) {
|
||||
this.methods = methods;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent a lambda expression
|
||||
*
|
||||
* eg. `() => null`
|
||||
*/
|
||||
class LambdaType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType[]} param_types
|
||||
* @param {ResolvedValue} return_type
|
||||
*/
|
||||
constructor(param_types, return_type) {
|
||||
this.param_types = param_types;
|
||||
this.return_type = return_type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent type name expressions
|
||||
*
|
||||
* eg. `x instanceof String`
|
||||
*/
|
||||
class TypeIdentType {
|
||||
/**
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent package name expressions
|
||||
*
|
||||
* eg. `java`
|
||||
*/
|
||||
class PackageNameType {
|
||||
/**
|
||||
* @param {string} package_name
|
||||
*/
|
||||
constructor(package_name) {
|
||||
this.package_name = package_name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Custom type used to represent an array literal
|
||||
*
|
||||
* eg. `new int[] { 1,2,3 }`
|
||||
*/
|
||||
class ArrayValueType {
|
||||
/**
|
||||
* @param {{tokens:Token[], value: ResolvedValue}[]} elements
|
||||
*/
|
||||
constructor(elements) {
|
||||
this.elements = elements;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent the types of a
|
||||
* expression that can return multiple distinct types
|
||||
*
|
||||
* eg. `x == null ? 0 : 'c'`
|
||||
*/
|
||||
class MultiValueType {
|
||||
/**
|
||||
* @param {ResolvedValue[]} types
|
||||
*/
|
||||
constructor(...types) {
|
||||
this.types = types;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./expressiontypes/literals/Number').NumberLiteral} NumberLiteral
|
||||
* @typedef {JavaType|MethodType|LambdaType|ArrayValueType|TypeIdentType|PackageNameType|MultiValueType|NumberLiteral} ResolvedValue
|
||||
**/
|
||||
|
||||
exports.AnyMethod = AnyMethod;
|
||||
exports.AnyType = AnyType;
|
||||
exports.AnyValue = AnyValue;
|
||||
exports.ArrayValueType = ArrayValueType;
|
||||
exports.LambdaType = LambdaType;
|
||||
exports.MethodType = MethodType;
|
||||
exports.MultiValueType = MultiValueType;
|
||||
exports.PackageNameType = PackageNameType;
|
||||
exports.TypeIdentType = TypeIdentType;
|
||||
1934
langserver/java/body-parser.js
Normal file
1934
langserver/java/body-parser.js
Normal file
File diff suppressed because it is too large
Load Diff
138
langserver/java/body-types.js
Normal file
138
langserver/java/body-types.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @typedef {import('./expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('./anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { JavaType, CEIType, ArrayType, Method } = require('java-mti');
|
||||
const { Token } = require('./tokenizer');
|
||||
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('./anys');
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
|
||||
class ResolvedIdent {
|
||||
/**
|
||||
* @param {string|Token} ident
|
||||
* @param {Expression[]} variables
|
||||
* @param {Method[]} methods
|
||||
* @param {JavaType[]} types
|
||||
* @param {string} package_name
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
constructor(ident, variables = [], methods = [], types = [], package_name = '', tokens = []) {
|
||||
this.source = ident instanceof Token ? ident.value : ident;
|
||||
this.variables = variables;
|
||||
this.methods = methods;
|
||||
this.types = types;
|
||||
this.package_name = package_name;
|
||||
this.tokens = ident instanceof Token ? [ident] : tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
if (this.variables[0]) {
|
||||
return this.variables[0].resolveExpression(ri);
|
||||
}
|
||||
if (this.methods[0]) {
|
||||
return new MethodType(this.methods);
|
||||
}
|
||||
if (this.types[0]) {
|
||||
return new TypeIdentType(this.types[0]);
|
||||
}
|
||||
if (this.package_name) {
|
||||
return new PackageNameType(this.package_name);
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.tokens, `Unresolved identifier: ${this.source}`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
class Local {
|
||||
/**
|
||||
* @param {Token[]} modifiers
|
||||
* @param {string} name
|
||||
* @param {Token} decltoken
|
||||
* @param {import('./source-types').SourceTypeIdent} typeIdent
|
||||
* @param {number} postnamearrdims
|
||||
* @param {ResolvedIdent} init
|
||||
*/
|
||||
constructor(modifiers, name, decltoken, typeIdent, postnamearrdims, init) {
|
||||
this.finalToken = modifiers.find(m => m.source === 'final') || null;
|
||||
this.name = name;
|
||||
this.decltoken = decltoken;
|
||||
if (postnamearrdims > 0) {
|
||||
typeIdent.resolved = new ArrayType(typeIdent.resolved, postnamearrdims);
|
||||
}
|
||||
this.typeIdent = typeIdent;
|
||||
this.init = init;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.typeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class Label {
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
this.name_token = token;
|
||||
}
|
||||
}
|
||||
|
||||
class MethodDeclarations {
|
||||
/** @type {Local[]} */
|
||||
locals = [];
|
||||
/** @type {Label[]} */
|
||||
labels = [];
|
||||
/** @type {import('./source-types').SourceType[]} */
|
||||
types = [];
|
||||
|
||||
_scopeStack = [];
|
||||
|
||||
pushScope() {
|
||||
this._scopeStack.push([this.locals, this.labels, this.types]);
|
||||
this.locals = this.locals.slice();
|
||||
this.labels = this.labels.slice();
|
||||
this.types = this.types.slice();
|
||||
}
|
||||
|
||||
popScope() {
|
||||
const prev = {
|
||||
locals: this.locals,
|
||||
labels: this.labels,
|
||||
types: this.types,
|
||||
};
|
||||
([this.locals, this.labels, this.types] = this._scopeStack.pop());
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveInfo {
|
||||
/**
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {*[]} problems
|
||||
*/
|
||||
constructor(typemap, problems) {
|
||||
this.typemap = typemap;
|
||||
this.problems = problems;
|
||||
}
|
||||
}
|
||||
|
||||
class ValidateInfo extends ResolveInfo {
|
||||
constructor(typemap, problems, method) {
|
||||
super(typemap, problems);
|
||||
this.method = method;
|
||||
/** @type {('if'|'else'|'for'|'while'|'do'|'switch'|'try'|'synchronized')[]} */
|
||||
this.statementStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
exports.Label = Label;
|
||||
exports.Local = Local;
|
||||
exports.MethodDeclarations = MethodDeclarations;
|
||||
exports.ResolvedIdent = ResolvedIdent;
|
||||
exports.ResolveInfo = ResolveInfo;
|
||||
exports.ValidateInfo = ValidateInfo;
|
||||
393
langserver/java/expression-resolver.js
Normal file
393
langserver/java/expression-resolver.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
* @typedef {import('./anys').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
|
||||
*/
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
const { TypeVariable, JavaType, PrimitiveType, NullType, ArrayType, CEIType, WildcardType, TypeVariableType, InferredTypeArgument } = require('java-mti');
|
||||
const { AnyType, ArrayValueType, LambdaType, MultiValueType } = require('./anys');
|
||||
const { ResolveInfo } = require('./body-types');
|
||||
const { NumberLiteral } = require('./expressiontypes/literals/Number');
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {ResolvedIdent} expression
|
||||
* @param {JavaType} assign_type
|
||||
*/
|
||||
function checkAssignment(ri, assign_type, expression) {
|
||||
const value = expression.resolveExpression(ri);
|
||||
checkTypeAssignable(assign_type, value, () => expression.tokens, ri.problems);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {ResolvedValue} value
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkTypeAssignable(variable_type, value, tokens, problems) {
|
||||
if (value instanceof NumberLiteral) {
|
||||
if (!value.isCompatibleWith(variable_type)) {
|
||||
incompatibleTypesError(variable_type, value.type, () => value.tokens(), problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value instanceof MultiValueType) {
|
||||
value.types.forEach(t => checkTypeAssignable(variable_type, t, tokens, problems));
|
||||
return;
|
||||
}
|
||||
if (value instanceof ArrayValueType) {
|
||||
checkArrayLiteral(variable_type, value, tokens, problems);
|
||||
return;
|
||||
}
|
||||
if (value instanceof LambdaType) {
|
||||
checkLambdaAssignable(variable_type, value, tokens, problems);
|
||||
return;
|
||||
}
|
||||
if (value instanceof JavaType) {
|
||||
if (!isTypeAssignable(variable_type, value)) {
|
||||
incompatibleTypesError(variable_type, value, tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens(), `Field, variable or method call expected`));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {JavaType} value_type
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function incompatibleTypesError(variable_type, value_type, tokens, problems) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Expression of type '${value_type.fullyDottedTypeName}' cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {LambdaType} value
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkLambdaAssignable(variable_type, value, tokens, problems) {
|
||||
const res = isLambdaAssignable(variable_type, value);
|
||||
if (res === true) {
|
||||
return;
|
||||
}
|
||||
switch (res[0]) {
|
||||
case 'non-interface':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Cannot assign lambda expression to type '${variable_type.fullyDottedTypeName}'`));
|
||||
return;
|
||||
case 'no-methods':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface '${variable_type.fullyDottedTypeName}' contains no abstract methods compatible with the specified lambda expression`));
|
||||
return;
|
||||
case 'param-count':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter counts`));
|
||||
return;
|
||||
case 'bad-param':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter types`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {LambdaType} value
|
||||
*/
|
||||
function isLambdaAssignable(variable_type, value) {
|
||||
if (!(variable_type instanceof CEIType) || variable_type.typeKind !== 'interface') {
|
||||
return ['non-interface'];
|
||||
}
|
||||
// the functional interface must only contain one abstract method excluding public Object methods
|
||||
// and ignoring type-compatible methods from superinterfaces.
|
||||
// this is quite complicated to calculate, so for now, just check against the most common case: a simple interface type with
|
||||
// a single abstract method
|
||||
if (variable_type.supers.length > 1) {
|
||||
return true;
|
||||
}
|
||||
if (variable_type.methods.length === 0) {
|
||||
return ['no-methods']
|
||||
}
|
||||
if (variable_type.methods.length > 1) {
|
||||
return true;
|
||||
}
|
||||
const intf_method = variable_type.methods[0];
|
||||
const intf_params = intf_method.parameters;
|
||||
if (intf_params.length !== value.param_types.length) {
|
||||
return ['param-count'];
|
||||
}
|
||||
|
||||
for (let i = 0; i < intf_params.length; i++) {
|
||||
// explicit parameter types must match exactly
|
||||
if (value.param_types[i] instanceof AnyType) {
|
||||
continue;
|
||||
}
|
||||
if (intf_params[i].type instanceof AnyType) {
|
||||
continue;
|
||||
}
|
||||
if (intf_params[i].type.typeSignature !== value.param_types[i].typeSignature) {
|
||||
return ['bad-param']
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {ArrayValueType} value_type
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkArrayLiteral(variable_type, value_type, tokens, problems) {
|
||||
if (!(variable_type instanceof ArrayType)) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Array expression cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
|
||||
return;
|
||||
}
|
||||
if (value_type.elements.length === 0) {
|
||||
// empty arrays are compatible with all array types
|
||||
return;
|
||||
}
|
||||
const element_type = variable_type.elementType;
|
||||
value_type.elements.forEach(element => {
|
||||
checkArrayElement(element_type, element.value, element.tokens);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {JavaType} element_type
|
||||
* @param {ResolvedValue} value_type
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
function checkArrayElement(element_type, value_type, tokens) {
|
||||
if (value_type instanceof NumberLiteral) {
|
||||
if (!value_type.isCompatibleWith(element_type)) {
|
||||
incompatibleTypesError(element_type, value_type.type, () => tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value_type instanceof JavaType) {
|
||||
if (!isTypeAssignable(element_type, value_type)) {
|
||||
incompatibleTypesError(element_type, value_type, () => tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value_type instanceof ArrayValueType) {
|
||||
checkArrayLiteral(element_type, value_type, () => tokens, problems);
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens, `Expression expected`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {ResolvedIdent} d
|
||||
* @param {'index'|'dimension'} kind
|
||||
*/
|
||||
function checkArrayIndex(ri, d, kind) {
|
||||
const idx = d.resolveExpression(ri);
|
||||
if (idx instanceof NumberLiteral) {
|
||||
if (!idx.isCompatibleWith(PrimitiveType.map.I)) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Value '${idx.toNumber()}' is not valid as an array ${kind}`));
|
||||
}
|
||||
else if (idx.toNumber() < 0) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Negative array ${kind}: ${idx.toNumber()}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (idx instanceof PrimitiveType) {
|
||||
if (!/^[BSI]$/.test(idx.typeSignature)) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Expression of type '${idx.label}' is not valid as an array ${kind}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Integer value expected`));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|NumberLiteral|LambdaType|MultiValueType} value_type
|
||||
*/
|
||||
function isTypeAssignable(dest_type, value_type) {
|
||||
|
||||
if (value_type instanceof NumberLiteral) {
|
||||
return value_type.isCompatibleWith(dest_type);
|
||||
}
|
||||
|
||||
if (value_type instanceof LambdaType) {
|
||||
return isLambdaAssignable(dest_type, value_type) === true;
|
||||
}
|
||||
|
||||
if (value_type instanceof MultiValueType) {
|
||||
return value_type.types.every(t => {
|
||||
if (t instanceof JavaType || t instanceof NumberLiteral || t instanceof LambdaType || t instanceof MultiValueType)
|
||||
return isTypeAssignable(dest_type, t);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (dest_type instanceof TypeVariableType) {
|
||||
is_assignable = !(value_type instanceof PrimitiveType || value_type instanceof NullType);
|
||||
}
|
||||
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 isTypeAssignable(dest_typevar.type, value_typevar_type);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ResolvedValue} value
|
||||
* @param {() => Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkBooleanBranchCondition(value, tokens, problems) {
|
||||
if (value instanceof JavaType) {
|
||||
if (!isTypeAssignable(PrimitiveType.map.Z, value)) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected, but type '${value.fullyDottedTypeName}' found.`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected.`));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {CEIType} type
|
||||
*/
|
||||
function getTypeInheritanceList(type) {
|
||||
const types = {
|
||||
/** @type {JavaType[]} */
|
||||
list: [type],
|
||||
/** @type {Set<JavaType>} */
|
||||
done: new Set(),
|
||||
};
|
||||
let object = null;
|
||||
for (let type; type = types.list.shift(); ) {
|
||||
// always add Object last
|
||||
if (type.rawTypeSignature === 'Ljava/lang/Object;') {
|
||||
object = type;
|
||||
continue;
|
||||
}
|
||||
if (types.done.has(type)) {
|
||||
continue;
|
||||
}
|
||||
types.done.add(type);
|
||||
if (type instanceof CEIType)
|
||||
types.list.push(...type.supers);
|
||||
}
|
||||
if (object) {
|
||||
types.done.add(object);
|
||||
}
|
||||
return Array.from(types.done);
|
||||
}
|
||||
|
||||
exports.checkArrayIndex = checkArrayIndex;
|
||||
exports.checkAssignment = checkAssignment;
|
||||
exports.checkBooleanBranchCondition = checkBooleanBranchCondition;
|
||||
exports.checkTypeAssignable = checkTypeAssignable;
|
||||
exports.getTypeInheritanceList = getTypeInheritanceList;
|
||||
exports.isTypeAssignable = isTypeAssignable;
|
||||
38
langserver/java/expressiontypes/ArrayIndexExpression.js
Normal file
38
langserver/java/expressiontypes/ArrayIndexExpression.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayType } = require('java-mti');
|
||||
const { checkArrayIndex } = require('../expression-resolver');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
class ArrayIndexExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {ResolvedIdent} index
|
||||
*/
|
||||
constructor(instance, index) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.instance.tokens, ...this.index.tokens];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const instance_type = this.instance.resolveExpression(ri);
|
||||
checkArrayIndex(ri, this.index, 'index');
|
||||
if (instance_type instanceof ArrayType) {
|
||||
return instance_type.elementType;
|
||||
}
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
exports.ArrayIndexExpression = ArrayIndexExpression;
|
||||
35
langserver/java/expressiontypes/ArrayValueExpression.js
Normal file
35
langserver/java/expressiontypes/ArrayValueExpression.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayValueType } = require('../anys');
|
||||
|
||||
class ArrayValueExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent[]} elements
|
||||
* @param {Token} open
|
||||
*/
|
||||
constructor(elements, open) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return new ArrayValueType(this.elements.map(e => ({
|
||||
tokens: e.tokens,
|
||||
value: e.resolveExpression(ri),
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
exports.ArrayValueExpression = ArrayValueExpression;
|
||||
200
langserver/java/expressiontypes/BinaryOpExpression.js
Normal file
200
langserver/java/expressiontypes/BinaryOpExpression.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
const { checkTypeAssignable } = require('../expression-resolver');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const operator = this.op.value;
|
||||
const lhsvalue = this.lhs.resolveExpression(ri);
|
||||
const rhsvalue = this.rhs.resolveExpression(ri);
|
||||
|
||||
if (lhsvalue instanceof AnyType || rhsvalue instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (lhsvalue instanceof NumberLiteral || rhsvalue instanceof NumberLiteral) {
|
||||
if (lhsvalue instanceof NumberLiteral && rhsvalue instanceof NumberLiteral) {
|
||||
// if they are both literals, compute the result
|
||||
if (/^[*/%+-]$/.test(operator)) {
|
||||
return NumberLiteral[operator](lhsvalue, rhsvalue);
|
||||
}
|
||||
if (/^([&|^]|<<|>>>?)$/.test(operator) && !/[FD]/.test(`${lhsvalue.type.typeSignature}${rhsvalue.type.typeSignature}`)) {
|
||||
return NumberLiteral[operator](lhsvalue, rhsvalue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'instanceof') {
|
||||
if (!(rhsvalue instanceof TypeIdentType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Type expected`));
|
||||
}
|
||||
if (!(lhsvalue instanceof JavaType || lhsvalue instanceof NumberLiteral)) {
|
||||
ri.problems.push(ParseProblem.Error(this.lhs.tokens, `Expression expected`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (/^([*/%&|^+-]?=|<<=|>>>?=)$/.test(operator)) {
|
||||
let src_type = rhsvalue;
|
||||
if (operator.length > 1) {
|
||||
const result_types = checkOperator(operator.slice(0,-1), ri, this.op, lhsvalue, rhsvalue);
|
||||
src_type = Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
|
||||
}
|
||||
if (lhsvalue instanceof JavaType) {
|
||||
checkTypeAssignable(lhsvalue, src_type, () => this.rhs.tokens, ri.problems);
|
||||
// result of assignments are lhs type
|
||||
return lhsvalue;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.op, `Invalid assignment`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
const result_types = checkOperator(operator, ri, this.op, lhsvalue, rhsvalue);
|
||||
return Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.lhs.tokens, this.op, ...this.rhs.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operator
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Token} operator_token
|
||||
* @param {ResolvedValue} lhstype
|
||||
* @param {ResolvedValue} rhstype
|
||||
* @returns {JavaType|JavaType[]}
|
||||
*/
|
||||
function checkOperator(operator, ri, operator_token, lhstype, rhstype) {
|
||||
|
||||
if (lhstype instanceof MultiValueType) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [];
|
||||
lhstype.types.reduce((arr, type) => {
|
||||
const types = checkOperator(operator, ri, operator_token, type, rhstype);
|
||||
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
|
||||
return arr;
|
||||
}, types);
|
||||
types = [...new Set(types)];
|
||||
return types.length === 1 ? types[0] : types;
|
||||
}
|
||||
|
||||
if (rhstype instanceof MultiValueType) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [];
|
||||
rhstype.types.reduce((arr, type) => {
|
||||
const types = checkOperator(operator, ri, operator_token, lhstype, type);
|
||||
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
|
||||
return arr;
|
||||
}, types);
|
||||
types = [...new Set(types)];
|
||||
return types.length === 1 ? types[0] : types;
|
||||
}
|
||||
|
||||
if (lhstype instanceof NumberLiteral) {
|
||||
lhstype = lhstype.type;
|
||||
}
|
||||
if (rhstype instanceof NumberLiteral) {
|
||||
rhstype = rhstype.type;
|
||||
}
|
||||
|
||||
if (!(lhstype instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
if (!(rhstype instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
const typekey = `${lhstype.typeSignature}#${rhstype.typeSignature}`;
|
||||
|
||||
if (operator === '+' && /(^|#)Ljava\/lang\/String;/.test(typekey)) {
|
||||
// string appending is compatible with all types
|
||||
return ri.typemap.get('java/lang/String');
|
||||
}
|
||||
|
||||
if (/^[*/%+-]$/.test(operator)) {
|
||||
// math operators - must be numeric
|
||||
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^(D|F#[^D]|J#[^FD]|I#[^JFD])/.test(typekey)) {
|
||||
return lhstype;
|
||||
}
|
||||
if (/^(.#D|.#F|.#J|.#I)/.test(typekey)) {
|
||||
return rhstype;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^(<<|>>>?)$/.test(operator)) {
|
||||
// shift operators - must be integral
|
||||
if (!/^[BSIJC]#[BSIJC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^J/.test(typekey)) {
|
||||
return PrimitiveType.map.J;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^[&|^]$/.test(operator)) {
|
||||
// bitwise or logical operators
|
||||
if (!/^[BSIJC]#[BSIJC]$|^Z#Z$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^[JZ]/.test(typekey)) {
|
||||
return lhstype;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^(&&|\|\|)$/.test(operator)) {
|
||||
// logical operators
|
||||
if (!/^Z#Z$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (/^(>=?|<=?)$/.test(operator)) {
|
||||
// numeric comparison operators
|
||||
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
// comparison operators
|
||||
if (typekey === 'Ljava/lang/String;#Ljava/lang/String;') {
|
||||
ri.problems.push(ParseProblem.Warning(operator_token, `Using equality operators '=='/'!=' to compare strings has unpredictable behaviour. Consider using String.equals(...) instead.`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
exports.BinaryOpExpression = BinaryOpExpression;
|
||||
28
langserver/java/expressiontypes/BracketedExpression.js
Normal file
28
langserver/java/expressiontypes/BracketedExpression.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
|
||||
class BracketedExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(expression) {
|
||||
super();
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.expression.resolveExpression(ri);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.expression.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
exports.BracketedExpression = BracketedExpression;
|
||||
137
langserver/java/expressiontypes/CastExpression.js
Normal file
137
langserver/java/expressiontypes/CastExpression.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { JavaType, PrimitiveType, NullType, CEIType, ArrayType } = require('java-mti');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
|
||||
class CastExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} castType
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(castType, expression) {
|
||||
super();
|
||||
this.castType = castType;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const cast_type = this.castType.resolveExpression(ri);
|
||||
if (cast_type instanceof TypeIdentType) {
|
||||
const expr_type = this.expression.resolveExpression(ri);
|
||||
checkCastable(this, cast_type.type, expr_type, ri.problems);
|
||||
return cast_type.type;
|
||||
}
|
||||
if (cast_type instanceof AnyType) {
|
||||
return cast_type;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.castType.tokens, 'Type expected'))
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.castType.tokens, ...this.expression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CastExpression} cast
|
||||
* @param {JavaType} cast_type
|
||||
* @param {ResolvedValue} expr_type
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkCastable(cast, cast_type, expr_type, problems) {
|
||||
if (expr_type instanceof JavaType) {
|
||||
if (!isTypeCastable(expr_type, cast_type)) {
|
||||
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: An expression of type '${expr_type.fullyDottedTypeName}' cannot be cast to type '${cast_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (expr_type instanceof NumberLiteral) {
|
||||
checkCastable(cast, cast_type, expr_type.type, problems);
|
||||
return;
|
||||
}
|
||||
if (expr_type instanceof MultiValueType) {
|
||||
expr_type.types.forEach(type => checkCastable(cast, cast_type, type, problems));
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: expression is not a value or variable`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 assignable to the other
|
||||
if (isTypeAssignable(source_type, cast_type)) {
|
||||
return true;
|
||||
}
|
||||
if (isTypeAssignable(cast_type, 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;
|
||||
}
|
||||
|
||||
exports.CastExpression = CastExpression;
|
||||
25
langserver/java/expressiontypes/Expression.js
Normal file
25
langserver/java/expressiontypes/Expression.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
|
||||
class Expression {
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
throw new Error('Expression.resolveExpression');
|
||||
}
|
||||
|
||||
/** @returns {Token|Token[]} */
|
||||
tokens() {
|
||||
throw new Error('Expression.tokens');
|
||||
}
|
||||
}
|
||||
|
||||
exports.Expression = Expression;
|
||||
41
langserver/java/expressiontypes/IncDecExpression.js
Normal file
41
langserver/java/expressiontypes/IncDecExpression.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const type = this.expr.resolveExpression(ri);
|
||||
if (type instanceof PrimitiveType) {
|
||||
if (/^[BSIJFD]$/.test(type.typeSignature)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.operator;
|
||||
}
|
||||
}
|
||||
|
||||
exports.IncDecExpression = IncDecExpression;
|
||||
50
langserver/java/expressiontypes/LambdaExpression.js
Normal file
50
langserver/java/expressiontypes/LambdaExpression.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { Block } = require('../statementtypes/Block');
|
||||
const { AnyType, LambdaType } = require('../anys');
|
||||
const { Local } = require('../body-types');
|
||||
|
||||
class LambdaExpression extends Expression {
|
||||
/**
|
||||
*
|
||||
* @param {(Local|ResolvedIdent)[]} params
|
||||
* @param {ResolvedIdent|Block} body
|
||||
*/
|
||||
constructor(params, body) {
|
||||
super();
|
||||
this.params = params;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
let return_type;
|
||||
if (this.body instanceof Block) {
|
||||
// todo - search for return statements to work out what return value the lambda has
|
||||
return_type = AnyType.Instance;
|
||||
} else {
|
||||
return_type = this.body.resolveExpression(ri);
|
||||
}
|
||||
const param_types = this.params.map(p => {
|
||||
if (p instanceof Local) {
|
||||
return p.type;
|
||||
}
|
||||
return AnyType.Instance;
|
||||
})
|
||||
return new LambdaType(param_types, return_type);
|
||||
|
||||
}
|
||||
|
||||
tokens() {
|
||||
if (this.body instanceof Block) {
|
||||
return this.body.open;
|
||||
}
|
||||
return this.body.tokens;
|
||||
}
|
||||
}
|
||||
exports.LambdaExpression = LambdaExpression;
|
||||
128
langserver/java/expressiontypes/MemberExpression.js
Normal file
128
langserver/java/expressiontypes/MemberExpression.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, CEIType, PrimitiveType } = require('java-mti');
|
||||
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('../anys');
|
||||
const { getTypeInheritanceList } = require('../expression-resolver');
|
||||
const { resolveNextPackage } = require('../type-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class MemberExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {Token} dot
|
||||
* @param {Token|null} member
|
||||
*/
|
||||
constructor(instance, dot, member) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.dot = dot;
|
||||
// member will be null for incomplete expressions
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
let instance = this.instance.resolveExpression(ri);
|
||||
|
||||
if (instance instanceof AnyType) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (instance instanceof PackageNameType) {
|
||||
this.dot.loc = { key: `fqdi:${instance.package_name}` };
|
||||
if (!this.member) {
|
||||
return instance;
|
||||
}
|
||||
this.member.loc = this.dot.loc;
|
||||
const ident = this.member.value;
|
||||
const { sub_package_name, type } = resolveNextPackage(instance.package_name, ident, ri.typemap);
|
||||
if (!type && !sub_package_name) {
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved identifier: '${ident}'`));
|
||||
}
|
||||
return type ? new TypeIdentType(type)
|
||||
: sub_package_name ? new PackageNameType(sub_package_name)
|
||||
: AnyType.Instance;
|
||||
}
|
||||
|
||||
let loc_key = `fqi`;
|
||||
if (instance instanceof TypeIdentType) {
|
||||
loc_key = 'fqs';
|
||||
instance = instance.type;
|
||||
}
|
||||
|
||||
if (!(instance instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
this.dot.loc = { key: `${loc_key}:${instance.typeSignature}` };
|
||||
if (!this.member) {
|
||||
ri.problems.push(ParseProblem.Error(this.dot, `Identifier expected`));
|
||||
return instance;
|
||||
}
|
||||
|
||||
this.member.loc = this.dot.loc;
|
||||
const ident = this.member.value;
|
||||
|
||||
if (ident === 'this') {
|
||||
// if this has a type qualifier (Type.this), return the type, otherwise it's
|
||||
// and error and return AnyType
|
||||
return ((loc_key === 'fqs') && (instance instanceof CEIType)) ? instance : AnyType.Instance;
|
||||
}
|
||||
|
||||
if (ident === 'class') {
|
||||
// if this has a type qualifier (Type.class), return the Class instance, otherwise it's
|
||||
// and error and return AnyType
|
||||
if (loc_key !== 'fqs') {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
let class_type = instance;
|
||||
if (instance instanceof PrimitiveType) {
|
||||
class_type = ri.typemap.get(`java/lang/${{
|
||||
B:'Byte',S:'Short',I:'Integer',J:'Long',F:'Float',D:'Double',C:'Character',Z:'Boolean',V:'Void'
|
||||
}[instance.typeSignature]}`)
|
||||
}
|
||||
const clz = ri.typemap.get('java/lang/Class').specialise([class_type]);
|
||||
if (!ri.typemap.has(clz.shortSignature)) {
|
||||
ri.typemap.set(clz.shortSignature, clz);
|
||||
}
|
||||
return clz;
|
||||
}
|
||||
|
||||
const field = instance.fields.find(f => f.name === ident);
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
|
||||
if (!(instance instanceof CEIType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
let methods = new Map();
|
||||
getTypeInheritanceList(instance).forEach(type => {
|
||||
type.methods.forEach(m => {
|
||||
let msig;
|
||||
if (m.name === ident && !methods.has(msig = m.methodSignature)) {
|
||||
methods.set(msig, m);
|
||||
}
|
||||
})
|
||||
});
|
||||
if (methods.size > 0) {
|
||||
return new MethodType([...methods.values()]);
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}' in type '${instance.fullyDottedRawName}'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.member;
|
||||
}
|
||||
}
|
||||
|
||||
exports.MemberExpression = MemberExpression;
|
||||
320
langserver/java/expressiontypes/MethodCallExpression.js
Normal file
320
langserver/java/expressiontypes/MethodCallExpression.js
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { AnyType, AnyMethod, LambdaType, MethodType, MultiValueType } = require('../anys');
|
||||
const { ArrayType, JavaType, Method,PrimitiveType, ReifiedConstructor, ReifiedMethod, Constructor } = require('java-mti');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
const { InstanceLiteral } = require('./literals/Instance')
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { ValidateInfo } = require('../body-types');
|
||||
const { SourceConstructor } = require('../source-types');
|
||||
|
||||
class MethodCallExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
*/
|
||||
constructor(instance, open_bracket, args, commas) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.open_bracket = open_bracket;
|
||||
this.args = args;
|
||||
this.commas = commas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const type = this.instance.resolveExpression(ri);
|
||||
if (type instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
if (!(type instanceof MethodType)) {
|
||||
// check if this is an aleternate or super constructor call: this() / super()
|
||||
const instance = this.instance.variables[0];
|
||||
if (!(instance instanceof InstanceLiteral) || !(type instanceof JavaType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.instance.tokens, `Expression is not a named method'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
let is_ctr = false;
|
||||
if (ri instanceof ValidateInfo) {
|
||||
is_ctr = ri.method instanceof SourceConstructor;
|
||||
}
|
||||
if (is_ctr) {
|
||||
resolveConstructorCall(ri, type.constructors, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
|
||||
} else {
|
||||
ri.problems.push(ParseProblem.Error(this.instance.tokens, `'this'/'super' constructor calls can only be used as the first statement of a constructor`));
|
||||
}
|
||||
return PrimitiveType.map.V;
|
||||
}
|
||||
|
||||
return resolveMethodCall(ri, type.methods, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.instance.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Method[]} methods
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function resolveMethodCall(ri, methods, open_bracket, args, commas, tokens) {
|
||||
const resolved_args = args.map((arg,idx) => arg.resolveExpression(ri));
|
||||
|
||||
// all the arguments must be typed expressions, number literals or lambdas
|
||||
/** @type {(JavaType|NumberLiteral|LambdaType|MultiValueType)[]} */
|
||||
const arg_types = [];
|
||||
resolved_args.forEach((a, idx) => {
|
||||
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType || a instanceof MultiValueType) {
|
||||
arg_types.push(a);
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
|
||||
// use AnyType for this argument
|
||||
arg_types.push(AnyType.Instance);
|
||||
});
|
||||
|
||||
// reify any methods with type-variables
|
||||
// - lambda expressions can't be used as type arguments so just pass them as void
|
||||
// - multi-value types will dynamically chhose the type, but it's always a reference type (so assignable to Object)
|
||||
const arg_java_types = arg_types.map(a =>
|
||||
a instanceof NumberLiteral ? a.type
|
||||
: a instanceof LambdaType ? PrimitiveType.map.V
|
||||
: a instanceof MultiValueType ? ri.typemap.get('java/lang/Object')
|
||||
: a);
|
||||
const reified_methods = methods.map(m => {
|
||||
if (m.typeVariables.length) {
|
||||
m = ReifiedMethod.build(m, arg_java_types);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
// work out which methods are compatible with the call arguments
|
||||
const compatible_methods = reified_methods.filter(m => isCallCompatible(m, arg_types));
|
||||
const return_types = new Set(compatible_methods.map(m => m.returnType));
|
||||
|
||||
// store the methods and argument position for signature help
|
||||
const methodIdx = Math.max(reified_methods.indexOf(compatible_methods[0]), 0);
|
||||
open_bracket.methodCallInfo = {
|
||||
methods: reified_methods,
|
||||
methodIdx,
|
||||
argIdx: 0,
|
||||
}
|
||||
args.forEach((arg, idx) => {
|
||||
const methodCallInfo = {
|
||||
methods: reified_methods,
|
||||
methodIdx,
|
||||
argIdx: idx,
|
||||
}
|
||||
// add the info to the previous comma
|
||||
const c = commas[idx-1];
|
||||
if (c) {
|
||||
c.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
// set the info on all the tokens used in the argument
|
||||
arg.tokens.forEach(tok => {
|
||||
if (tok.methodCallInfo === null) {
|
||||
tok.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!compatible_methods[0]) {
|
||||
// if any of the arguments is AnyType, just return AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
const methodlist = reified_methods.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`No compatible method found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
|
||||
));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (compatible_methods.length > 1) {
|
||||
// if any of the arguments is AnyType, return the known return-type or AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
|
||||
}
|
||||
// see if we have an exact match
|
||||
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
|
||||
const exact_match = compatible_methods.find(m => m.methodSignature.startsWith(callsig));
|
||||
if (exact_match) {
|
||||
compatible_methods.splice(0, compatible_methods.length, exact_match);
|
||||
}
|
||||
}
|
||||
|
||||
if (compatible_methods.length > 1) {
|
||||
const methodlist = compatible_methods.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`Ambiguous method call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
|
||||
));
|
||||
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
|
||||
}
|
||||
|
||||
return compatible_methods[0].returnType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Constructor[]} constructors
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function resolveConstructorCall(ri, constructors, open_bracket, args, commas, tokens) {
|
||||
const resolved_args = args.map(arg => arg.resolveExpression(ri));
|
||||
|
||||
// all the arguments must be typed expressions, number literals or lambdas
|
||||
/** @type {(JavaType|NumberLiteral|LambdaType)[]} */
|
||||
const arg_types = [];
|
||||
resolved_args.forEach((a, idx) => {
|
||||
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType) {
|
||||
arg_types.push(a);
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
|
||||
// use AnyType for this argument
|
||||
arg_types.push(AnyType.Instance);
|
||||
});
|
||||
|
||||
// reify any methods with type-variables
|
||||
// - lambda expressions can't be used as type arguments so just pass them as void
|
||||
const arg_java_types = arg_types.map(a =>
|
||||
a instanceof NumberLiteral ? a.type
|
||||
: a instanceof LambdaType ? PrimitiveType.map.V
|
||||
: a);
|
||||
const reifed_ctrs = constructors.map(c => {
|
||||
if (c.typeVariables.length) {
|
||||
c = ReifiedConstructor.build(c, arg_java_types);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
// work out which methods are compatible with the call arguments
|
||||
const compatible_ctrs = reifed_ctrs.filter(m => isCallCompatible(m, arg_types));
|
||||
|
||||
// store the methods and argument position for signature help
|
||||
const methodIdx = reifed_ctrs.indexOf(compatible_ctrs[0]);
|
||||
open_bracket.methodCallInfo = {
|
||||
methods: reifed_ctrs,
|
||||
methodIdx,
|
||||
argIdx: 0,
|
||||
}
|
||||
args.forEach((arg, idx) => {
|
||||
const methodCallInfo = {
|
||||
methods: reifed_ctrs,
|
||||
methodIdx,
|
||||
argIdx: idx,
|
||||
}
|
||||
// add the info to the previous comma
|
||||
const c = commas[idx-1];
|
||||
if (c) {
|
||||
c.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
// set the info on all the tokens used in the argument
|
||||
arg.tokens.forEach(tok => {
|
||||
if (tok.methodCallInfo === null) {
|
||||
tok.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!compatible_ctrs[0]) {
|
||||
// if any of the arguments is AnyType, just ignore the call
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return;
|
||||
}
|
||||
const ctrlist = reifed_ctrs.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`No compatible constructor found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (compatible_ctrs.length > 1) {
|
||||
// if any of the arguments is AnyType, return the known return-type or AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return;
|
||||
}
|
||||
// see if we have an exact match
|
||||
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
|
||||
const exact_match = compatible_ctrs.find(m => m.methodSignature.startsWith(callsig));
|
||||
if (exact_match) {
|
||||
compatible_ctrs.splice(0, compatible_ctrs.length, exact_match);
|
||||
}
|
||||
}
|
||||
|
||||
if (compatible_ctrs.length > 1) {
|
||||
const ctrlist = compatible_ctrs.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`Ambiguous constructor call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Method|Constructor} m
|
||||
* @param {(JavaType | NumberLiteral | LambdaType | MultiValueType)[]} arg_types
|
||||
*/
|
||||
function isCallCompatible(m, arg_types) {
|
||||
if (m instanceof AnyMethod) {
|
||||
return true;
|
||||
}
|
||||
const param_count = m.parameterCount;
|
||||
if (param_count !== arg_types.length) {
|
||||
// for variable arity methods, we must have at least n-1 formal parameters
|
||||
if (!m.isVariableArity || arg_types.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 < arg_types.length; i++) {
|
||||
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 (arg_types.length === param_count) {
|
||||
if (isTypeAssignable(param_type, arg_types[i])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
param_type = param_type.elementType;
|
||||
}
|
||||
// is the argument assignable to the parameter
|
||||
if (isTypeAssignable(param_type, arg_types[i])) {
|
||||
continue;
|
||||
}
|
||||
// mismatch parameter type
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.MethodCallExpression = MethodCallExpression;
|
||||
exports.resolveConstructorCall = resolveConstructorCall;
|
||||
94
langserver/java/expressiontypes/NewExpression.js
Normal file
94
langserver/java/expressiontypes/NewExpression.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../source-types').AnonymousSourceType} AnonymousSourceType
|
||||
* @typedef {import('../source-types').SourceTypeIdent} SourceTypeIdent
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayType } = require('java-mti');
|
||||
const { FixedLengthArrayType, SourceArrayType } = require('../source-types');
|
||||
const { checkArrayIndex } = require('../expression-resolver');
|
||||
const { resolveConstructorCall } = require('./MethodCallExpression');
|
||||
|
||||
class NewArray extends Expression {
|
||||
/**
|
||||
* @param {Token} new_token
|
||||
* @param {SourceTypeIdent} element_type
|
||||
* @param {ResolvedIdent} dimensions
|
||||
*/
|
||||
constructor(new_token, element_type, dimensions) {
|
||||
super();
|
||||
this.new_token = new_token;
|
||||
this.element_type = element_type;
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
/** @type {ResolvedIdent[]} */
|
||||
const fixed_dimensions = [];
|
||||
const type = this.dimensions.types[0];
|
||||
for (let x = type; ;) {
|
||||
if (x instanceof FixedLengthArrayType) {
|
||||
fixed_dimensions.unshift(x.length);
|
||||
x = x.parent_type;
|
||||
continue;
|
||||
}
|
||||
if (x instanceof SourceArrayType) {
|
||||
x = x.parent_type;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const arrdims = type instanceof ArrayType ? type.arrdims : 1;
|
||||
const array_type = new ArrayType(this.element_type.resolved, arrdims);
|
||||
|
||||
fixed_dimensions.forEach(d => {
|
||||
checkArrayIndex(ri, d, 'dimension');
|
||||
})
|
||||
return array_type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.new_token, ...this.dimensions.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
class NewObject extends Expression {
|
||||
/**
|
||||
* @param {Token} new_token
|
||||
* @param {SourceTypeIdent} object_type
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {Token[]} commas
|
||||
* @param {AnonymousSourceType} type_body
|
||||
*/
|
||||
constructor(new_token, object_type, open_bracket, ctr_args, commas, type_body) {
|
||||
super();
|
||||
this.new_token = new_token;
|
||||
this.object_type = object_type;
|
||||
this.open_bracket = open_bracket;
|
||||
this.ctr_args = ctr_args;
|
||||
this.commas = commas;
|
||||
this.type_body = type_body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
resolveConstructorCall(ri, this.object_type.resolved.constructors, this.open_bracket, this.ctr_args, this.commas, () => this.tokens());
|
||||
return this.object_type.resolved;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.new_token, ...this.object_type.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
exports.NewArray = NewArray;
|
||||
exports.NewObject = NewObject;
|
||||
35
langserver/java/expressiontypes/TernaryOpExpression.js
Normal file
35
langserver/java/expressiontypes/TernaryOpExpression.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { MultiValueType } = require('../anys');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const ttype = this.truthExpression.resolveExpression(ri);
|
||||
const ftype = this.falseExpression.resolveExpression(ri);
|
||||
return new MultiValueType(ttype, ftype);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.test.tokens, ...this.truthExpression.tokens, ...this.falseExpression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
exports.TernaryOpExpression = TernaryOpExpression;
|
||||
96
langserver/java/expressiontypes/UnaryOpExpression.js
Normal file
96
langserver/java/expressiontypes/UnaryOpExpression.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType } = require('../anys');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
|
||||
class UnaryOpExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} expression
|
||||
* @param {Token} op
|
||||
*/
|
||||
constructor(expression, op) {
|
||||
super();
|
||||
this.expression = expression;
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const operator = this.op.value;
|
||||
const value = this.expression.resolveExpression(ri);
|
||||
|
||||
if (value instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (value instanceof NumberLiteral) {
|
||||
if (/^[+-]$/.test(operator)) {
|
||||
return NumberLiteral[operator](value);
|
||||
}
|
||||
if (/^[!~]$/.test(operator) && value.type.typeSignature === 'I') {
|
||||
return NumberLiteral[operator](value);
|
||||
}
|
||||
}
|
||||
|
||||
const type = value instanceof JavaType ? value : value instanceof NumberLiteral ? value.type : null;
|
||||
|
||||
if (!type) {
|
||||
ri.problems.push(ParseProblem.Error(this.expression.tokens, `Expression expected`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
return checkOperator(operator, ri, this.op, type);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.op, ...this.expression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operator
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Token} operator_token
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
function checkOperator(operator, ri, operator_token, type) {
|
||||
|
||||
let is_valid = false;
|
||||
/** @type {JavaType} */
|
||||
let return_type = AnyType.Instance;
|
||||
|
||||
if (/^[+-]$/.test(operator)) {
|
||||
// math operators - must be numeric
|
||||
is_valid = /^[BSIJFDC]$/.test(type.typeSignature);
|
||||
return_type = type;
|
||||
}
|
||||
|
||||
if (/^~$/.test(operator)) {
|
||||
// bitwise invert operator - must be integral
|
||||
is_valid = /^[BSIJC]$/.test(type.typeSignature);
|
||||
return_type = PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^!$/.test(operator)) {
|
||||
// logical not operator - must be boolean
|
||||
is_valid = /^Z$/.test(type.typeSignature);
|
||||
return_type = PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (!is_valid) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for type '${type.fullyDottedTypeName}'`));
|
||||
}
|
||||
|
||||
return return_type;
|
||||
}
|
||||
|
||||
exports.UnaryOpExpression = UnaryOpExpression;
|
||||
36
langserver/java/expressiontypes/Variable.js
Normal file
36
langserver/java/expressiontypes/Variable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').Field} Field
|
||||
* @typedef {import('java-mti').Parameter} Parameter
|
||||
* @typedef {import('../source-types').SourceEnumValue} SourceEnumValue
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
|
||||
class Variable extends Expression {
|
||||
/**
|
||||
* @param {Token} name_token
|
||||
* @param {Local|Parameter|Field|SourceEnumValue} variable
|
||||
*/
|
||||
constructor(name_token, variable) {
|
||||
super();
|
||||
this.name_token = name_token;
|
||||
this.variable = variable;
|
||||
this.type = this.variable.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.name_token;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Variable = Variable;
|
||||
17
langserver/java/expressiontypes/literals/Boolean.js
Normal file
17
langserver/java/expressiontypes/literals/Boolean.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
class BooleanLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, PrimitiveType.map.Z);
|
||||
}
|
||||
}
|
||||
|
||||
exports.BooleanLiteral = BooleanLiteral;
|
||||
17
langserver/java/expressiontypes/literals/Character.js
Normal file
17
langserver/java/expressiontypes/literals/Character.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
class CharacterLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, PrimitiveType.map.C);
|
||||
}
|
||||
}
|
||||
|
||||
exports.CharacterLiteral = CharacterLiteral;
|
||||
31
langserver/java/expressiontypes/literals/Instance.js
Normal file
31
langserver/java/expressiontypes/literals/Instance.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
|
||||
class InstanceLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token 'this' or 'super' token
|
||||
* @param {CEIType} scoped_type
|
||||
*/
|
||||
constructor(token, scoped_type) {
|
||||
super(token, null);
|
||||
this.token = token;
|
||||
this.scoped_type = scoped_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
if (this.token.value === 'this') {
|
||||
return this.scoped_type;
|
||||
}
|
||||
return this.scoped_type.supers.find(t => t.typeKind === 'class') || ri.typemap.get('java/lang/Object');
|
||||
}
|
||||
}
|
||||
|
||||
exports.InstanceLiteral = InstanceLiteral;
|
||||
33
langserver/java/expressiontypes/literals/LiteralValue.js
Normal file
33
langserver/java/expressiontypes/literals/LiteralValue.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('../../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { Expression } = require('../Expression');
|
||||
|
||||
class LiteralValue extends Expression {
|
||||
/**
|
||||
* @param {Token|Token[]} tokens
|
||||
* @param {JavaType} known_type
|
||||
*/
|
||||
constructor(tokens, known_type) {
|
||||
super();
|
||||
this._tokens = tokens;
|
||||
this.type = known_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this._tokens;
|
||||
}
|
||||
}
|
||||
|
||||
exports.LiteralValue = LiteralValue;
|
||||
17
langserver/java/expressiontypes/literals/Null.js
Normal file
17
langserver/java/expressiontypes/literals/Null.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { NullType } = require('java-mti');
|
||||
|
||||
class NullLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, new NullType());
|
||||
}
|
||||
}
|
||||
|
||||
exports.NullLiteral = NullLiteral;
|
||||
236
langserver/java/expressiontypes/literals/Number.js
Normal file
236
langserver/java/expressiontypes/literals/Number.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
/**
|
||||
* NumberLiteral is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc).
|
||||
*
|
||||
* It allows 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 NumberLiteral extends LiteralValue {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {string} kind
|
||||
* @param {PrimitiveType} default_type
|
||||
* @param {string} [value]
|
||||
*/
|
||||
constructor(tokens, kind, default_type, value = tokens[0].value) {
|
||||
super(tokens, default_type);
|
||||
this.value = value;
|
||||
this.numberKind = kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {string} kind
|
||||
* @param {PrimitiveType} type
|
||||
* @param {number} value
|
||||
*/
|
||||
static calc(a, b, kind, type, value) {
|
||||
let atoks = a.tokens(), btoks = b.tokens();
|
||||
atoks = Array.isArray(atoks) ? atoks : [atoks];
|
||||
btoks = Array.isArray(btoks) ? btoks : [btoks];
|
||||
return new NumberLiteral([...atoks, ...btoks], kind, type, value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
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 NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
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;
|
||||
const type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I;
|
||||
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {string} opvalue
|
||||
* @param {(a) => Number} op
|
||||
*/
|
||||
static unary(a, opvalue, op) {
|
||||
if (opvalue === '-') {
|
||||
const ai = a.toNumber();
|
||||
if (ai === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai);
|
||||
const type = PrimitiveType.map[a.type.typeSignature];
|
||||
const toks = a.tokens();
|
||||
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
|
||||
}
|
||||
const ai = a.toInt();
|
||||
if (ai === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai);
|
||||
const type = /J/.test(a.type.typeSignature) ? PrimitiveType.map.J : PrimitiveType.map.I;
|
||||
const toks = a.tokens();
|
||||
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
static math(a, b, op) {
|
||||
const ai = a.toNumber(), bi = b.toNumber();
|
||||
let val = op(ai, bi);
|
||||
const typekey = a.type.typeSignature + b.type.typeSignature;
|
||||
if (!/[FD]/.test(typekey)) {
|
||||
val = Math.trunc(val);
|
||||
}
|
||||
const type = typekey.includes('D') ? PrimitiveType.map.D
|
||||
: typekey.includes('F') ? PrimitiveType.map.F
|
||||
: typekey.includes('J') ? PrimitiveType.map.J
|
||||
: PrimitiveType.map.I;
|
||||
// note: Java allows integer division by zero at compile-time - it will
|
||||
// always cause an ArithmeticException at runtime, so the result here (inf or nan)
|
||||
// is largely meaningless
|
||||
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
static '~'(value) { return NumberLiteral.unary(value, '~', (a) => ~a) }
|
||||
static '+'(lhs, rhs) { return !rhs
|
||||
? lhs // unary e.g +5
|
||||
: NumberLiteral.math(lhs, rhs, (a,b) => a + b)
|
||||
}
|
||||
static '-'(lhs, rhs) { return !rhs
|
||||
? NumberLiteral.unary(lhs, '-', (a) => -a)
|
||||
: NumberLiteral.math(lhs, rhs, (a,b) => a - b)
|
||||
}
|
||||
static '*'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a * b) }
|
||||
static '/'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a / b) }
|
||||
static '%'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a % b) }
|
||||
static '&'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a & b) }
|
||||
static '|'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a | b) }
|
||||
static '^'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a ^ b) }
|
||||
static '>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a >> b) }
|
||||
static '>>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => {
|
||||
// unsigned shift (>>>) is not supported by bigints
|
||||
// @ts-ignore
|
||||
return (a >> b) & ~(-1n << (64n - b));
|
||||
}) }
|
||||
static '<<'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a << b) }
|
||||
|
||||
toInt() {
|
||||
switch (this.numberKind) {
|
||||
case 'hex-number-literal':
|
||||
case 'int-number-literal':
|
||||
// unlike parseInt, BigInt doesn't like invalid characters, so
|
||||
// ensure we strip any trailing long specifier
|
||||
return BigInt(this.value.match(/(.+?)[lL]?$/)[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
toNumber() {
|
||||
return parseFloat(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.value !== '0x') {
|
||||
const non_leading_zero_digits = this.value.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.value.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 NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.D);
|
||||
case 'hex-number-literal':
|
||||
return new NumberLiteral([token], token.kind, suffix(' Ll') || PrimitiveType.map.I);
|
||||
case 'int-number-literal':
|
||||
default:
|
||||
return new NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.I);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.NumberLiteral = NumberLiteral;
|
||||
18
langserver/java/expressiontypes/literals/String.js
Normal file
18
langserver/java/expressiontypes/literals/String.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
|
||||
class StringLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
* @param {CEIType} string_type
|
||||
*/
|
||||
constructor(token, string_type) {
|
||||
super(token, string_type);
|
||||
}
|
||||
}
|
||||
|
||||
exports.StringLiteral = StringLiteral;
|
||||
109
langserver/java/import-resolver.js
Normal file
109
langserver/java/import-resolver.js
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
const ResolvedImport = require('./parsetypes/resolved-import');
|
||||
|
||||
/**
|
||||
* Search a space-separated list of type names for values that match a dotted import.
|
||||
*
|
||||
* @param {string} typenames newline-separated list of fully qualified type names
|
||||
* @param {string} dotted_import fully-qualified import name (e.g "java.util")
|
||||
* @param {boolean} demandload true if this is a demand-load import
|
||||
*/
|
||||
function fetchImportedTypes(typenames, dotted_import, demandload) {
|
||||
const matcher = demandload
|
||||
// for demand-load, we search for any types that begin with the specified import name
|
||||
// - note that after the import text, only words and $ are allowed (because additional dots would imply a subpackage)
|
||||
? new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}[/$][\\w$]+$`, 'gm')
|
||||
// for exact-load, we search for any types that precisely matches the specified import name
|
||||
: new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}$`, 'gm');
|
||||
|
||||
// run the regex against the list of type names
|
||||
const matching_names = typenames.match(matcher);
|
||||
return matching_names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a single parsed import
|
||||
*
|
||||
* @param {Map<string, import('java-mti').CEIType>} typemap
|
||||
* @param {string} dotted_name
|
||||
* @param {boolean} is_static
|
||||
* @param {boolean} on_demand
|
||||
* @param {'owner-package'|'import'|'implicit-import'} import_kind
|
||||
*/
|
||||
function resolveSingleImport(typemap, dotted_name, is_static, on_demand, import_kind) {
|
||||
// construct the list of typenames
|
||||
const typenames = [...typemap.keys()].join('\n');
|
||||
|
||||
if (is_static) {
|
||||
if (on_demand) {
|
||||
// import all static members - the dotted name must be an exact type
|
||||
const matches = fetchImportedTypes(typenames, dotted_name, false);
|
||||
if (matches) {
|
||||
return new ResolvedImport(matches, '*', typemap, import_kind);
|
||||
}
|
||||
} else if (dotted_name.includes('.')) {
|
||||
// the final ident is the static member - the rest is the exact type
|
||||
const split_name = dotted_name.match(/(.+)\.([^.]+)$/);
|
||||
const matches = fetchImportedTypes(typenames, split_name[1], false);
|
||||
if (matches) {
|
||||
const i = new ResolvedImport(matches, split_name[2], typemap, import_kind);
|
||||
// if there's no matching member, treat it as an invalid import
|
||||
if (i.members.length > 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const matches = fetchImportedTypes(typenames, dotted_name, on_demand);
|
||||
if (matches) {
|
||||
return new ResolvedImport(matches, null, typemap, import_kind);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a set of imports for a module.
|
||||
*
|
||||
* Note that the order of the resolved imports is important for correct type resolution:
|
||||
* - same-package imports are first,
|
||||
* - followed by import declarations (in order of declaration),
|
||||
* - followed by implicit packages
|
||||
*
|
||||
* @param {Map<string, import('java-mti').CEIType>} typemap
|
||||
* @param {string} package_name package name of the module
|
||||
* @param {string[]} [implicitPackages] list of implicit demand-load packages
|
||||
*/
|
||||
function resolveImports(typemap, package_name, implicitPackages = ['java.lang']) {
|
||||
|
||||
// construct the list of typenames
|
||||
const typenames = [...typemap.keys()].join('\n');
|
||||
|
||||
/** @type {ResolvedImport[]} */
|
||||
const resolved = [];
|
||||
|
||||
// import types matching the current package
|
||||
if (package_name) {
|
||||
const matches = fetchImportedTypes(typenames, package_name, true);
|
||||
if (matches)
|
||||
resolved.push(new ResolvedImport(matches, null, typemap, 'owner-package'));
|
||||
}
|
||||
|
||||
// import types from the implicit packages
|
||||
implicitPackages.forEach(package_name => {
|
||||
const matches = fetchImportedTypes(typenames, package_name, true);
|
||||
if (matches)
|
||||
resolved.push(new ResolvedImport(matches, null, typemap, 'implicit-import'));
|
||||
})
|
||||
|
||||
/**
|
||||
* return the resolved imports.
|
||||
*/
|
||||
return resolved;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveImports,
|
||||
resolveSingleImport,
|
||||
ResolvedImport,
|
||||
}
|
||||
72
langserver/java/java-libraries.js
Normal file
72
langserver/java/java-libraries.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { CEIType, loadJavaLibraryCacheFile } = require('java-mti');
|
||||
const analytics = require('../analytics');
|
||||
const { trace, time, timeEnd } = require('../logging');
|
||||
|
||||
/**
|
||||
* @param {string} extensionPath install path of extension
|
||||
* @param {string[]} additional_libs set of androidx library names to include eg: ["androidx.activity:activity"]
|
||||
* @returns {Promise<Map<string,CEIType>>}
|
||||
*/
|
||||
async function loadAndroidSystemLibrary(extensionPath, additional_libs) {
|
||||
analytics.time('android-library-load');
|
||||
time('android-library-load');
|
||||
let library;
|
||||
try {
|
||||
if (!extensionPath) {
|
||||
throw new Error('Missing extension path')
|
||||
}
|
||||
const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
|
||||
trace(`loading android library from ${cache_folder} with androidx libs: ${JSON.stringify(additional_libs)}`)
|
||||
const typemap = await loadJavaLibraryCacheFile(path.join(cache_folder, 'android-29.zip'));
|
||||
if (Array.isArray(additional_libs) && additional_libs.length) {
|
||||
await loadJavaLibraryCacheFile(path.join(cache_folder, 'androidx-20200701.zip'), additional_libs, typemap);
|
||||
}
|
||||
trace(`loaded ${typemap.size} types into android library`);
|
||||
library = typemap;
|
||||
} finally {
|
||||
timeEnd('android-library-load');
|
||||
analytics.timeEnd('android-library-load', 'ms', { libs: additional_libs, typecount: library ? library.size : 0 });
|
||||
}
|
||||
return library;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cache_folder
|
||||
*/
|
||||
async function loadHighestAPIPlatform(cache_folder) {
|
||||
/** @type {fs.Dirent[]} */
|
||||
const files = await new Promise((res, rej) => {
|
||||
fs.readdir(cache_folder, {withFileTypes: true}, (err, files) => err ? rej(err) : res(files));
|
||||
});
|
||||
|
||||
// find the file with the highest API level
|
||||
let best_match = {
|
||||
api: 0,
|
||||
/** @type {fs.Dirent} */
|
||||
file: null,
|
||||
};
|
||||
files.forEach(file => {
|
||||
const m = file.name.match(/^android-(\d+)\.zip$/);
|
||||
if (!m) return;
|
||||
const api = parseInt(m[1], 10);
|
||||
if (api > best_match.api) {
|
||||
best_match = {
|
||||
api,
|
||||
file,
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!best_match.file) {
|
||||
throw new Error(`No valid platform cache files found in ${cache_folder}`)
|
||||
}
|
||||
console.log(`loading android platform cache: ${best_match.file.name}`);
|
||||
|
||||
const cache_file = path.join(cache_folder, best_match.file.name);
|
||||
const typemap = loadJavaLibraryCacheFile(cache_file);
|
||||
|
||||
return typemap;
|
||||
}
|
||||
|
||||
exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary;
|
||||
73
langserver/java/parsetypes/parse-problem.js
Normal file
73
langserver/java/parsetypes/parse-problem.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const ProblemSeverity = require('./problem-severity');
|
||||
const { TextBlock } = require('./textblock');
|
||||
|
||||
/**
|
||||
* @typedef {import('./problem-severity').Severity} Severity
|
||||
*/
|
||||
|
||||
|
||||
class ParseProblem {
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
* @param {Severity} severity
|
||||
*/
|
||||
constructor(token, message, severity) {
|
||||
if (!token || (Array.isArray(token) && !token[0])) {
|
||||
this.startIdx = 0;
|
||||
this.endIdx = 1;
|
||||
}
|
||||
else if (Array.isArray(token)) {
|
||||
this.startIdx = token[0].range.start;
|
||||
const lastToken = token[token.length - 1];
|
||||
this.endIdx = lastToken.range.start + lastToken.range.length;
|
||||
} else {
|
||||
this.startIdx = token.range.start;
|
||||
this.endIdx = this.startIdx + token.range.length;
|
||||
}
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Error(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Warning(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Information(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Information);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Hint(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
*/
|
||||
static syntaxError(token) {
|
||||
if (!token) return null;
|
||||
return ParseProblem.Error(token, 'Unsupported, invalid or incomplete declaration');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParseProblem;
|
||||
8
langserver/java/parsetypes/problem-severity.js
Normal file
8
langserver/java/parsetypes/problem-severity.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @typedef {1|2|3|4} Severity
|
||||
* @type {{ Error:1, Warning:2, Information:3, Hint:4 }}
|
||||
* these match the vscode DiagnosticSeverity values
|
||||
*/
|
||||
const ProblemSeverity = { Error:1, Warning:2, Information:3, Hint:4 };
|
||||
|
||||
module.exports = ProblemSeverity;
|
||||
49
langserver/java/parsetypes/resolved-import.js
Normal file
49
langserver/java/parsetypes/resolved-import.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a resolved import.
|
||||
*
|
||||
* Each instance holds an array of types that would be resolved by the specified import.
|
||||
* Each type is mapped to a JavaType which lists the implementation details of the type (fields, methods, etc).
|
||||
*
|
||||
*/
|
||||
class ResolvedImport {
|
||||
/**
|
||||
* @param {RegExpMatchArray} matches
|
||||
* @param {string} static_ident
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {'owner-package'|'import'|'implicit-import'} import_kind
|
||||
*/
|
||||
constructor(matches, static_ident, typemap, import_kind) {
|
||||
/**
|
||||
* Array of fully qualified type names in JRE format resolved in this import
|
||||
*/
|
||||
this.fullyQualifiedNames = Array.from(matches);
|
||||
|
||||
/**
|
||||
* THe map of fully-qualified type names to JavaTypes
|
||||
*/
|
||||
this.types = new Map(matches.map(name => [name, typemap.get(name)]));
|
||||
|
||||
this.members = [];
|
||||
if (static_ident) {
|
||||
const type = typemap.get(matches[0]);
|
||||
if (type) {
|
||||
type.fields.forEach(f => f.modifiers.includes('static') && (static_ident === '*' || static_ident === f.name) && this.members.push(f));
|
||||
type.methods.forEach(m => m.modifiers.includes('static') && (static_ident === '*' || static_ident === m.name) && this.members.push(m));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What kind of import this is:
|
||||
* - `"owner-package"`: types that are implicitly imported from the same package as the declared module
|
||||
* - `"import"`: types that are inclduded via an import declaration specified in the module
|
||||
* - `"implicit-import"`: types that are included without any explicit import (`java.lang.*` for example)
|
||||
*/
|
||||
this.import_kind = import_kind;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResolvedImport;
|
||||
141
langserver/java/parsetypes/textblock.js
Normal file
141
langserver/java/parsetypes/textblock.js
Normal file
@@ -0,0 +1,141 @@
|
||||
class BlockRange {
|
||||
|
||||
get end() { return this.start + this.length }
|
||||
get text() { return this.source.slice(this.start, this.end) }
|
||||
/**
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor(source, start, length) {
|
||||
this.source = source;
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
class TextBlock {
|
||||
/**
|
||||
* @param {BlockRange|TextBlockArray} range
|
||||
* @param {string} simplified
|
||||
*/
|
||||
constructor(range, simplified) {
|
||||
this.range = range;
|
||||
this.simplified = simplified;
|
||||
}
|
||||
|
||||
blockArray() {
|
||||
return this.range instanceof TextBlockArray ? this.range : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the original source
|
||||
* @returns {number}
|
||||
*/
|
||||
get length() {
|
||||
return this.range.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
* @param {string} [simplified]
|
||||
*/
|
||||
static from(source, start, length, simplified) {
|
||||
const range = new BlockRange(source, start, length);
|
||||
return new TextBlock(range, simplified || range.text);
|
||||
}
|
||||
|
||||
get source() { return this.toSource() }
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toSource() {
|
||||
return this.range instanceof BlockRange
|
||||
? this.range.text
|
||||
: this.range.toSource()
|
||||
}
|
||||
}
|
||||
|
||||
class TextBlockArray {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {import('../tokenizer').Token[]} [blocks]
|
||||
*/
|
||||
constructor(id, blocks = []) {
|
||||
this.id = id;
|
||||
this.blocks = blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the original source
|
||||
* @returns {number}
|
||||
*/
|
||||
get length() {
|
||||
return this.blocks.reduce(((len,b) => len + b.length), 0);
|
||||
}
|
||||
|
||||
get simplified() {
|
||||
return this.blocks.map(tb => tb.simplified).join('');
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
get start() {
|
||||
return this.blocks[0].range.start;
|
||||
}
|
||||
|
||||
sourcemap() {
|
||||
let idx = 0;
|
||||
const parts = [];
|
||||
/** @type {number[]} */
|
||||
const map = this.blocks.reduce((arr,tb,i) => {
|
||||
arr[idx] = i;
|
||||
if (!tb) {
|
||||
throw this.blocks;
|
||||
}
|
||||
parts.push(tb.simplified);
|
||||
idx += tb.simplified.length;
|
||||
return arr;
|
||||
}, []);
|
||||
map[idx] = this.blocks.length;
|
||||
return {
|
||||
simplified: parts.join(''),
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {number} start_block_idx
|
||||
* @param {number} block_count
|
||||
* @param {RegExpMatchArray} match
|
||||
* @param {string} marker
|
||||
* @param {*} [parseClass]
|
||||
* @param {boolean} [pad]
|
||||
*/
|
||||
shrink(id, start_block_idx, block_count, match, marker, parseClass, pad=true) {
|
||||
if (block_count <= 0) return;
|
||||
const collapsed = new TextBlockArray(id, this.blocks.splice(start_block_idx, block_count, null));
|
||||
const simplified = pad
|
||||
? collapsed.source.replace(/./g, ' ').replace(/^./, marker)
|
||||
: marker;
|
||||
return this.blocks[start_block_idx] = parseClass
|
||||
? new parseClass(collapsed, simplified, match)
|
||||
: new TextBlock(collapsed, simplified);
|
||||
}
|
||||
|
||||
get source() { return this.toSource() }
|
||||
|
||||
toSource() {
|
||||
return this.blocks.map(tb => tb.toSource()).join('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BlockRange,
|
||||
TextBlock,
|
||||
TextBlockArray,
|
||||
}
|
||||
756
langserver/java/source-types.js
Normal file
756
langserver/java/source-types.js
Normal file
@@ -0,0 +1,756 @@
|
||||
const { CEIType, JavaType, PrimitiveType, ArrayType, TypeVariableType, Field, Method, MethodBase, Constructor, Parameter, TypeVariable, TypeArgument } = require('java-mti');
|
||||
const { AnyType } = require('./anys');
|
||||
const { Token } = require('./tokenizer');
|
||||
|
||||
/**
|
||||
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser|string} scope_or_package_name
|
||||
* @param {string} name
|
||||
*/
|
||||
function generateShortSignature(scope_or_package_name, name) {
|
||||
if (scope_or_package_name instanceof SourceType) {
|
||||
const type = scope_or_package_name;
|
||||
return `${type._rawShortSignature}$${name}`;
|
||||
}
|
||||
if (scope_or_package_name instanceof SourceMethod
|
||||
|| scope_or_package_name instanceof SourceConstructor
|
||||
|| scope_or_package_name instanceof SourceInitialiser) {
|
||||
const method = scope_or_package_name;
|
||||
return `${method.owner._rawShortSignature}$${method.owner.localTypeCount += 1}${name}`;
|
||||
}
|
||||
const pkgname = scope_or_package_name;
|
||||
return pkgname ?`${pkgname.replace(/\./g, '/')}/${name}` : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} enum_type
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function createImplicitEnumMethods(enum_type, typemap) {
|
||||
return [
|
||||
new class extends Method {
|
||||
constructor() {
|
||||
super(enum_type, 'values', ['public','static'], '');
|
||||
this._returnType = new ArrayType(enum_type, 1);
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
},
|
||||
new class extends Method {
|
||||
constructor() {
|
||||
super(enum_type, 'valueOf', ['public','static'], '');
|
||||
this._parameters = [
|
||||
new Parameter('name', typemap.get('java/lang/String'), false)
|
||||
]
|
||||
this._returnType = enum_type;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class SourceType extends CEIType {
|
||||
/**
|
||||
* @param {string} rawShortSignature
|
||||
* @param {'class'|'interface'|'enum'|'@interface'} typeKind
|
||||
* @param {string[]|number} modifiers
|
||||
* @param {string} docs
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(rawShortSignature, typeKind, modifiers, docs, typemap) {
|
||||
super(rawShortSignature, typeKind, modifiers, docs);
|
||||
/**
|
||||
* Number of local/anonymous types declared in the scope of this type
|
||||
* The number is used when naming them.
|
||||
*/
|
||||
this.localTypeCount = 0;
|
||||
/** @type {SourceTypeIdent[]} */
|
||||
this.extends_types = [];
|
||||
/** @type {SourceTypeIdent[]} */
|
||||
this.implements_types = [];
|
||||
/** @type {SourceConstructor[]} */
|
||||
this.constructors = [];
|
||||
/** @type {Method[]} */
|
||||
this.methods = typeKind === 'enum'
|
||||
? createImplicitEnumMethods(this, typemap)
|
||||
: [];
|
||||
/** @type {SourceField[]} */
|
||||
this.fields = [];
|
||||
/** @type {SourceInitialiser[]} */
|
||||
this.initers = [];
|
||||
/** @type {SourceEnumValue[]} */
|
||||
this.enumValues = [];
|
||||
this.typemap = typemap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} docs
|
||||
* @param {Token} ident
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {SourceType} anonymousType
|
||||
*/
|
||||
addEnumValue(docs, ident, ctr_args, anonymousType) {
|
||||
this.enumValues.push(new SourceEnumValue(this, docs, ident, ctr_args, anonymousType));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceMethod[]}
|
||||
*/
|
||||
get sourceMethods() {
|
||||
// @ts-ignore
|
||||
return this.methods.filter(m => m instanceof SourceMethod);// [...this.implicitMethods, ...this.sourceMethods];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AnonymousSourceType extends SourceType {
|
||||
|
||||
/**
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} scope
|
||||
*/
|
||||
static genSignature(scope) {
|
||||
const type = scope instanceof SourceType ? scope : scope.owner;
|
||||
return `${type._rawShortSignature}$${type.localTypeCount += 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceTypeIdent} typeident
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(typeident, outer_scope, typemap) {
|
||||
super(AnonymousSourceType.genSignature(outer_scope), 'class', [], '', typemap);
|
||||
this.simpleTypeName = typeident.resolved.simpleTypeName;
|
||||
this.typeIdent = typeident;
|
||||
}
|
||||
|
||||
get dottedTypeName() {
|
||||
return this.typeIdent.resolved.dottedTypeName;
|
||||
}
|
||||
|
||||
get fullyDottedRawName() {
|
||||
return this.dottedTypeName;
|
||||
}
|
||||
|
||||
get fullyDottedTypeName() {
|
||||
return this.dottedTypeName;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return `new ${this.dottedTypeName}`;
|
||||
}
|
||||
|
||||
/** @type {JavaType[]} */
|
||||
get supers() {
|
||||
if (this.typeIdent.resolved instanceof AnyType || this.typeIdent.resolved.typeKind !== 'class') {
|
||||
return [this.typemap.get('java/lang/Object')]
|
||||
}
|
||||
return [this.typeIdent.resolved];
|
||||
}
|
||||
|
||||
get shortSignature() {
|
||||
return this._rawShortSignature;
|
||||
}
|
||||
|
||||
get rawTypeSignature() {
|
||||
return `L${this._rawShortSignature};`;
|
||||
}
|
||||
|
||||
get typeSignature() {
|
||||
return this.rawTypeSignature;
|
||||
}
|
||||
}
|
||||
|
||||
class NamedSourceType extends SourceType {
|
||||
/**
|
||||
* @param {string} packageName
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {string} typeKind
|
||||
* @param {Token} kind_token
|
||||
* @param {Token} name_token
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(packageName, outer_scope, docs, modifiers, typeKind, kind_token, name_token, typemap) {
|
||||
// @ts-ignore
|
||||
super(generateShortSignature(outer_scope || packageName, name_token.value), typeKind, modifiers.map(m => m.source), docs, typemap);
|
||||
super.packageName = packageName;
|
||||
this.modifierTokens = modifiers;
|
||||
this.kind_token = kind_token;
|
||||
this.nameToken = name_token;
|
||||
this.scope = outer_scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} package_name
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {string} name
|
||||
*/
|
||||
static getShortSignature(package_name, outer_scope, name) {
|
||||
return generateShortSignature(outer_scope || package_name || '', name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
*/
|
||||
setModifierTokens(mods) {
|
||||
this.modifierTokens = mods;
|
||||
this.modifiers = mods.map(m => m.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType[]} types
|
||||
* @returns {CEIType}
|
||||
*/
|
||||
specialise(types) {
|
||||
const short_sig = `${this.shortSignature}<${types.map(t => t.typeSignature).join('')}>`;
|
||||
if (this.typemap.has(short_sig)) {
|
||||
// @ts-ignore
|
||||
return this.typemap.get(short_sig);
|
||||
}
|
||||
/** @type {'class'|'enum'|'interface'|'@interface'} */
|
||||
// @ts-ignore
|
||||
const typeKind = this.typeKind;
|
||||
const specialised_type = new SpecialisedSourceType(this, typeKind, this._rawShortSignature, types);
|
||||
this.typemap.set(short_sig, specialised_type);
|
||||
return specialised_type;
|
||||
}
|
||||
|
||||
get supers() {
|
||||
const supertypes = [...this.extends_types, ...this.implements_types].map(x => x.resolved);
|
||||
if (this.typeKind === 'enum') {
|
||||
/** @type {CEIType} */
|
||||
const enumtype = this.typemap.get('java/lang/Enum');
|
||||
supertypes.unshift(enumtype.specialise([this]));
|
||||
}
|
||||
else if (!supertypes.find(type => type.typeKind === 'class')) {
|
||||
supertypes.unshift(this.typemap.get('java/lang/Object'));
|
||||
}
|
||||
return supertypes;
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialisedSourceType extends CEIType {
|
||||
/**
|
||||
*
|
||||
* @param {SourceType} source_type
|
||||
* @param {'class'|'enum'|'interface'|'@interface'} typeKind
|
||||
* @param {string} raw_short_signature
|
||||
* @param {JavaType[]} types
|
||||
*/
|
||||
constructor(source_type, typeKind, raw_short_signature, types) {
|
||||
super(raw_short_signature, typeKind, source_type.modifiers, source_type.docs);
|
||||
this.source_type = source_type;
|
||||
this.typemap = source_type.typemap;
|
||||
/** @type {TypeArgument[]} */
|
||||
// @ts-ignore
|
||||
const type_args = source_type.typeVariables.map((tv, idx) => new TypeArgument(this, tv, types[idx] || this.typemap.get('java/lang/Object')));
|
||||
this.typeVariables = type_args;
|
||||
|
||||
function resolveType(type, typevars = []) {
|
||||
if (type instanceof ArrayType) {
|
||||
return new ArrayType(resolveType(type.base, typevars), type.arrdims);
|
||||
}
|
||||
if (!(type instanceof TypeVariableType)) {
|
||||
return type;
|
||||
}
|
||||
if (typevars.includes(type.typeVariable)) {
|
||||
return type;
|
||||
}
|
||||
const specialised_type = type_args.find(ta => ta.name === type.typeVariable.name);
|
||||
return specialised_type.type;
|
||||
}
|
||||
|
||||
this.fields = source_type.fields.map(f => {
|
||||
const type = this;
|
||||
return new class extends Field {
|
||||
constructor() {
|
||||
super(f.modifiers, f.docs);
|
||||
this.owner = type;
|
||||
this.source = f;
|
||||
this.fieldType = resolveType(f.fieldTypeIdent.resolved);
|
||||
}
|
||||
get name() { return this.source.name }
|
||||
get type() { return this.fieldType }
|
||||
};
|
||||
});
|
||||
|
||||
this.constructors = source_type.constructors.map(c => {
|
||||
const type = this;
|
||||
return new class extends Constructor {
|
||||
constructor() {
|
||||
super(type, c.modifiers, c.docs);
|
||||
this.owner = type;
|
||||
this.source = c;
|
||||
this._parameters = c.sourceParameters.map(p => new Parameter(p.name, resolveType(p.paramTypeIdent.resolved, c.typeVariables), p.varargs));
|
||||
}
|
||||
get hasImplementation() {
|
||||
return !!this.source.body;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get typeVariables() {
|
||||
return this.source.typeVars;
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
this.methods = source_type.methods.map(method => {
|
||||
if (!(method instanceof SourceMethod)) {
|
||||
return method;
|
||||
}
|
||||
const m = method;
|
||||
const type = this;
|
||||
return new class extends Method {
|
||||
constructor() {
|
||||
super(type, m.name, m.modifiers, m.docs);
|
||||
this.owner = type;
|
||||
this.source = m;
|
||||
this._returnType = resolveType(m.returnType, m.typeVars)
|
||||
this._parameters = m.sourceParameters.map(p => new Parameter(p.name, resolveType(p.type, m.typeVars), p.varargs));
|
||||
}
|
||||
get hasImplementation() {
|
||||
return !!this.source.body;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
get typeVariables() {
|
||||
return this.source.typeVars;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType[]} types
|
||||
* @returns {CEIType}
|
||||
*/
|
||||
specialise(types) {
|
||||
const short_sig = `${this._rawShortSignature}<${types.map(t => t.typeSignature).join('')}>`;
|
||||
if (this.typemap.has(short_sig)) {
|
||||
// @ts-ignore
|
||||
return this.typemap.get(short_sig);
|
||||
}
|
||||
/** @type {'class'|'enum'|'interface'|'@interface'} */
|
||||
// @ts-ignore
|
||||
const typeKind = this.typeKind;
|
||||
const specialised_type = new SpecialisedSourceType(this.source_type, typeKind, this._rawShortSignature, types);
|
||||
this.typemap.set(short_sig, specialised_type);
|
||||
return specialised_type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SourceEnumValue extends Field {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token} ident
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {SourceType} anonymousType
|
||||
*/
|
||||
constructor(owner, docs, ident, ctr_args, anonymousType) {
|
||||
super(['public','static','final'], docs);
|
||||
this.owner = owner;
|
||||
this.ident = ident;
|
||||
this.value = ctr_args;
|
||||
this.anonymousType = anonymousType;
|
||||
}
|
||||
|
||||
get label() {
|
||||
// don't include the implicit modifiers in the label
|
||||
return `${this.owner.simpleTypeName} ${this.name}`;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.ident.value;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.owner;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceTypeIdent {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
constructor(tokens, type) {
|
||||
this.tokens = tokens;
|
||||
this.resolved = type;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceField extends Field {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceTypeIdent} field_type_ident
|
||||
* @param {Token} name_token
|
||||
* @param {ResolvedIdent} init
|
||||
*/
|
||||
constructor(owner, docs, modifiers, field_type_ident, name_token, init) {
|
||||
super(modifiers.map(m => m.value), docs);
|
||||
this.owner = owner;
|
||||
this.modifierTokens = modifiers;
|
||||
this.fieldTypeIdent = field_type_ident;
|
||||
this.nameToken = name_token;
|
||||
this.init = init;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.nameToken ? this.nameToken.value : '';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.fieldTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceConstructor extends Constructor {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {TypeVariable[]} type_vars
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceParameter[]} parameters
|
||||
* @param {JavaType[]} throws
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, type_vars, modifiers, parameters, throws, body_tokens) {
|
||||
super(owner, modifiers.map(m => m.value), docs);
|
||||
this.owner = owner;
|
||||
this.typeVars = type_vars;
|
||||
this.modifierTokens = modifiers;
|
||||
this.sourceParameters = parameters;
|
||||
this.throws = throws;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
get hasImplementation() {
|
||||
return !!this.body;
|
||||
}
|
||||
|
||||
get parameterCount() {
|
||||
return this.sourceParameters.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return this.sourceParameters;
|
||||
}
|
||||
|
||||
get typeVariables() {
|
||||
return this.typeVars;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceMethod extends Method {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {TypeVariable[]} type_vars
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceAnnotation[]} annotations
|
||||
* @param {SourceTypeIdent} method_type_ident
|
||||
* @param {Token} name_token
|
||||
* @param {SourceParameter[]} parameters
|
||||
* @param {JavaType[]} throws
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, type_vars, modifiers, annotations, method_type_ident, name_token, parameters, throws, body_tokens) {
|
||||
super(owner, name_token ? name_token.value : '', modifiers.map(m => m.value), docs);
|
||||
this.annotations = annotations;
|
||||
this.owner = owner;
|
||||
this.typeVars = type_vars;
|
||||
this.modifierTokens = modifiers;
|
||||
this.returnTypeIdent = method_type_ident;
|
||||
this.nameToken = name_token;
|
||||
this.sourceParameters = parameters;
|
||||
this.throws = throws;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
get hasImplementation() {
|
||||
return !!this.body;
|
||||
}
|
||||
|
||||
get parameterCount() {
|
||||
return this.sourceParameters.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return this.sourceParameters;
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return this.returnTypeIdent.resolved;
|
||||
}
|
||||
|
||||
get typeVariables() {
|
||||
return this.typeVars;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceInitialiser extends MethodBase {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, modifiers, body_tokens) {
|
||||
super(owner, modifiers.map(m => m.value), docs);
|
||||
/** @type {SourceType} */
|
||||
this.owner = owner;
|
||||
this.modifierTokens = modifiers;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return [];
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return PrimitiveType.map.V;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceParameter extends Parameter {
|
||||
/**
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceTypeIdent} typeident
|
||||
* @param {boolean} varargs
|
||||
* @param {Token} name_token
|
||||
*/
|
||||
constructor(modifiers, typeident, varargs, name_token) {
|
||||
super(name_token ? name_token.value : '', typeident.resolved, varargs);
|
||||
this.nameToken = name_token;
|
||||
this.modifierTokens = modifiers;
|
||||
this.paramTypeIdent = typeident;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.paramTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceAnnotation {
|
||||
/**
|
||||
* @param {SourceTypeIdent} typeident
|
||||
*/
|
||||
constructor(typeident) {
|
||||
this.annotationTypeIdent = typeident;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.annotationTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourcePackage {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(tokens, name) {
|
||||
this.tokens = tokens;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceImport {
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {Token[]} name_tokens
|
||||
* @param {string} pkg_name
|
||||
* @param {Token} static_token
|
||||
* @param {Token} asterisk_token
|
||||
* @param {import('./parsetypes/resolved-import')} resolved
|
||||
*/
|
||||
constructor(tokens, name_tokens, pkg_name, static_token, asterisk_token, resolved) {
|
||||
this.tokens = tokens;
|
||||
this.nameTokens = name_tokens;
|
||||
this.package_name = pkg_name;
|
||||
this.staticToken = static_token;
|
||||
this.asteriskToken = asterisk_token;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
get isDemandLoad() {
|
||||
return !!this.asteriskToken;
|
||||
}
|
||||
|
||||
get isStatic() {
|
||||
return !!this.staticToken;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceUnit {
|
||||
/** @type {string} */
|
||||
uri = '';
|
||||
/** @type {Token[]} */
|
||||
tokens = [];
|
||||
/** @type {SourcePackage} */
|
||||
package_ = null;
|
||||
/** @type {SourceImport[]} */
|
||||
imports = [];
|
||||
/** @type {SourceType[]} */
|
||||
types = [];
|
||||
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
getSourceMethodAtToken(token) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
for (let type of this.types) {
|
||||
for (let method of [...type.sourceMethods, ...type.constructors, ...type.initers]) {
|
||||
if (method.body && method.body.tokens && method.body.tokens.includes(token)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} char_index
|
||||
*/
|
||||
getTokenAt(char_index) {
|
||||
let i = 0;
|
||||
for (let tok of this.tokens) {
|
||||
if (char_index > tok.range.start + tok.range.length) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
while (i > 0 && tok.kind === 'wsc') {
|
||||
tok = this.tokens[--i];
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} char_index
|
||||
*/
|
||||
getCompletionOptionsAt(char_index) {
|
||||
const token = this.getTokenAt(char_index);
|
||||
const method = this.getSourceMethodAtToken(token);
|
||||
// we should also include local variables here, but
|
||||
// it's currently difficult to map an individual token to a scope
|
||||
return {
|
||||
index: char_index,
|
||||
loc: token && token.loc,
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the package this unit belongs to
|
||||
*/
|
||||
get packageName() {
|
||||
return (this.package_ && this.package_.name) || '';
|
||||
}
|
||||
}
|
||||
|
||||
class SourceArrayType extends ArrayType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} element_type
|
||||
*/
|
||||
constructor(element_type) {
|
||||
super(element_type, 1);
|
||||
this.parent_type = element_type;
|
||||
}
|
||||
get label() {
|
||||
return `${this.parent_type.label}[]`;
|
||||
}
|
||||
}
|
||||
|
||||
class FixedLengthArrayType extends SourceArrayType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} element_type
|
||||
* @param {ResolvedIdent} length
|
||||
*/
|
||||
constructor(element_type, length) {
|
||||
super(element_type);
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return `${this.parent_type.label}[${this.length.source}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMethodLike
|
||||
*/
|
||||
|
||||
exports.SourceType = SourceType;
|
||||
exports.SourceTypeIdent = SourceTypeIdent;
|
||||
exports.SourceField = SourceField;
|
||||
exports.SourceMethod = SourceMethod;
|
||||
exports.SourceParameter = SourceParameter;
|
||||
exports.SourceConstructor = SourceConstructor;
|
||||
exports.SourceInitialiser = SourceInitialiser;
|
||||
exports.SourceAnnotation = SourceAnnotation;
|
||||
exports.SourceUnit = SourceUnit;
|
||||
exports.SourcePackage = SourcePackage;
|
||||
exports.SourceImport = SourceImport;
|
||||
exports.SourceEnumValue = SourceEnumValue;
|
||||
exports.SourceArrayType = SourceArrayType;
|
||||
exports.FixedLengthArrayType = FixedLengthArrayType;
|
||||
exports.NamedSourceType = NamedSourceType;
|
||||
exports.AnonymousSourceType = AnonymousSourceType;
|
||||
37
langserver/java/statement-validater.js
Normal file
37
langserver/java/statement-validater.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
const { CEIType } = require('java-mti')
|
||||
const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
|
||||
|
||||
const { Block } = require("./statementtypes/Block");
|
||||
const { Statement } = require("./statementtypes/Statement");
|
||||
const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement");
|
||||
|
||||
const { ValidateInfo } = require('./body-types');
|
||||
|
||||
/**
|
||||
* @param {Block} block
|
||||
* @param {SourceMethod | SourceConstructor | SourceInitialiser} method
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkStatementBlock(block, method, typemap, problems) {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
block.validate(new ValidateInfo(typemap, problems, method));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Statement} statement
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
function checkNonVarDeclStatement(statement, vi) {
|
||||
if (statement instanceof LocalDeclStatement) {
|
||||
vi.problems.push(ParseProblem.Error(statement.locals[0].decltoken, `Local variables cannot be declared as single conditional statements`));
|
||||
};
|
||||
statement.validate(vi);
|
||||
}
|
||||
|
||||
exports.checkStatementBlock = checkStatementBlock;
|
||||
exports.checkNonVarDeclStatement = checkNonVarDeclStatement;
|
||||
39
langserver/java/statementtypes/AssertStatement.js
Normal file
39
langserver/java/statementtypes/AssertStatement.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
|
||||
class AssertStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
message = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.expression) {
|
||||
const value = this.expression.resolveExpression(vi);
|
||||
if (!(value instanceof JavaType) || !isTypeAssignable(PrimitiveType.map.Z, value)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Boolean expression expected`));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.message) {
|
||||
const msg_value = this.message.resolveExpression(vi);
|
||||
if (!(msg_value instanceof JavaType)) {
|
||||
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression expected`));
|
||||
} else if (msg_value === PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression type cannot be 'void'`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.AssertStatement = AssertStatement;
|
||||
46
langserver/java/statementtypes/Block.js
Normal file
46
langserver/java/statementtypes/Block.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').Label} Label
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceType} SourceType
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class Block extends Statement {
|
||||
/** @type {Statement[]} */
|
||||
statements = [];
|
||||
|
||||
/** @type {{locals: Local[], labels: Label[], types: SourceType[]}} */
|
||||
decls = null;
|
||||
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} open
|
||||
*/
|
||||
constructor(owner, open) {
|
||||
super(owner);
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.decls) {
|
||||
const locals = this.decls.locals.reverse();
|
||||
locals.forEach(local => {
|
||||
if (locals.find(l => l.name === local.name) !== local) {
|
||||
vi.problems.push(ParseProblem.Error(local.decltoken, `Variable redeclared: ${local.name}`))
|
||||
}
|
||||
});
|
||||
}
|
||||
for (let statement of this.statements) {
|
||||
statement.validate(vi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Block = Block;
|
||||
23
langserver/java/statementtypes/BreakStatement.js
Normal file
23
langserver/java/statementtypes/BreakStatement.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class BreakStatement extends KeywordStatement {
|
||||
/** @type {Token} */
|
||||
target = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!vi.statementStack.find(s => /^(for|do|while|switch)$/.test(s))) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `break can only be specified inside loop/switch statements`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.BreakStatement = BreakStatement;
|
||||
23
langserver/java/statementtypes/ContinueStatement.js
Normal file
23
langserver/java/statementtypes/ContinueStatement.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ContinueStatement extends KeywordStatement {
|
||||
/** @type {Token} */
|
||||
target = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!vi.statementStack.find(s => /^(for|do|while)$/.test(s))) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `continue can only be specified inside loop statements`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ContinueStatement = ContinueStatement;
|
||||
31
langserver/java/statementtypes/DoStatement.js
Normal file
31
langserver/java/statementtypes/DoStatement.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('../statementtypes/Block').Block} Block
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
|
||||
class DoStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Block} */
|
||||
block = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.block) {
|
||||
vi.statementStack.unshift('do');
|
||||
this.block.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
}
|
||||
|
||||
exports.DoStatement = DoStatement;
|
||||
6
langserver/java/statementtypes/EmptyStatement.js
Normal file
6
langserver/java/statementtypes/EmptyStatement.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
class EmptyStatement extends Statement {
|
||||
}
|
||||
|
||||
exports.EmptyStatement = EmptyStatement;
|
||||
42
langserver/java/statementtypes/ExpressionStatement.js
Normal file
42
langserver/java/statementtypes/ExpressionStatement.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const { BinaryOpExpression } = require('../expressiontypes/BinaryOpExpression');
|
||||
const { MethodCallExpression } = require('../expressiontypes/MethodCallExpression');
|
||||
const { NewObject } = require('../expressiontypes/NewExpression');
|
||||
const { IncDecExpression } = require('../expressiontypes/IncDecExpression');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ExpressionStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(owner, expression) {
|
||||
super(owner);
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
// only method calls, new objects, increments and assignments are allowed as expression statements
|
||||
const e = this.expression.variables[0];
|
||||
let is_statement = e instanceof MethodCallExpression || e instanceof NewObject || e instanceof IncDecExpression;
|
||||
if (e instanceof BinaryOpExpression) {
|
||||
is_statement = e.op.kind === 'assignment-operator';
|
||||
}
|
||||
if (!is_statement) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Statement expected`));
|
||||
}
|
||||
this.expression.resolveExpression(vi);
|
||||
}
|
||||
}
|
||||
|
||||
exports.ExpressionStatement = ExpressionStatement;
|
||||
55
langserver/java/statementtypes/ForStatement.js
Normal file
55
langserver/java/statementtypes/ForStatement.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkNonVarDeclStatement } = require('../statement-validater');
|
||||
const { Local, ResolvedIdent } = require('../body-types');
|
||||
|
||||
class ForStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent[] | Local[]} */
|
||||
init = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {ResolvedIdent[]} */
|
||||
update = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
iterable = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.init) {
|
||||
this.init.forEach(x => {
|
||||
if (x instanceof ResolvedIdent) {
|
||||
x.resolveExpression(vi);
|
||||
} else if (x instanceof Local) {
|
||||
if (x.init) {
|
||||
x.init.resolveExpression(vi);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.test) {
|
||||
this.test.resolveExpression(vi);
|
||||
}
|
||||
if (this.update) {
|
||||
this.update.forEach(e => e.resolveExpression(vi));
|
||||
}
|
||||
if (this.iterable) {
|
||||
this.iterable.resolveExpression(vi);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('for');
|
||||
checkNonVarDeclStatement(this.statement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ForStatement = ForStatement;
|
||||
39
langserver/java/statementtypes/IfStatement.js
Normal file
39
langserver/java/statementtypes/IfStatement.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
const { checkNonVarDeclStatement } = require('../statement-validater');
|
||||
|
||||
class IfStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
/** @type {Statement} */
|
||||
elseStatement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.test) {
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('if');
|
||||
checkNonVarDeclStatement(this.statement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
if (this.elseStatement) {
|
||||
vi.statementStack.unshift('else');
|
||||
checkNonVarDeclStatement(this.elseStatement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.IfStatement = IfStatement;
|
||||
18
langserver/java/statementtypes/InvalidStatement.js
Normal file
18
langserver/java/statementtypes/InvalidStatement.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
class InvalidStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(owner, token) {
|
||||
super(owner);
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
exports.InvalidStatement = InvalidStatement;
|
||||
22
langserver/java/statementtypes/KeywordStatement.js
Normal file
22
langserver/java/statementtypes/KeywordStatement.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
/**
|
||||
* A statement that begins with a keyword (if, do, while, etc)
|
||||
*/
|
||||
class KeywordStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} keyword
|
||||
*/
|
||||
constructor(owner, keyword) {
|
||||
super(owner);
|
||||
this.keyword = keyword;
|
||||
}
|
||||
}
|
||||
|
||||
exports.KeywordStatement = KeywordStatement;
|
||||
35
langserver/java/statementtypes/LocalDeclStatement.js
Normal file
35
langserver/java/statementtypes/LocalDeclStatement.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').Label} Label
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceType} SourceType
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { checkAssignment } = require('../expression-resolver');
|
||||
|
||||
class LocalDeclStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Local[]} locals
|
||||
*/
|
||||
constructor(owner, locals) {
|
||||
super(owner);
|
||||
this.locals = locals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
this.locals.forEach(local => {
|
||||
if (local.init) {
|
||||
checkAssignment(vi, local.type, local.init);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.LocalDeclStatement = LocalDeclStatement;
|
||||
61
langserver/java/statementtypes/ReturnStatement.js
Normal file
61
langserver/java/statementtypes/ReturnStatement.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
const { LambdaType, MultiValueType } = require('../anys');
|
||||
|
||||
class ReturnStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
const method_return_type = vi.method.returnType;
|
||||
if (!this.expression) {
|
||||
if (method_return_type !== PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `Method must return a value of type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method_return_type === PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `void method cannot return a value`));
|
||||
return;
|
||||
}
|
||||
const type = this.expression.resolveExpression(vi);
|
||||
checkType(type, () => this.expression.tokens);
|
||||
|
||||
/**
|
||||
* @param {ResolvedValue} type
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function checkType(type, tokens) {
|
||||
if (type instanceof JavaType || type instanceof NumberLiteral) {
|
||||
if (!isTypeAssignable(method_return_type, type)) {
|
||||
const expr_type = type instanceof NumberLiteral ? type.type : type;
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: expression of type '${expr_type.fullyDottedTypeName}' cannot be returned from a method of type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else if (type instanceof MultiValueType) {
|
||||
// ternary, eg. return x > 0 ? 1 : 2;
|
||||
type.types.forEach(type => checkType(type, tokens));
|
||||
} else if (type instanceof LambdaType) {
|
||||
if (!isTypeAssignable(method_return_type, type)) {
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: lambda expression is not compatible with method type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `'${method_return_type.fullyDottedTypeName}' type expression expected`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ReturnStatement = ReturnStatement;
|
||||
17
langserver/java/statementtypes/Statement.js
Normal file
17
langserver/java/statementtypes/Statement.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
|
||||
class Statement {
|
||||
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
*/
|
||||
constructor(owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
validate(vi) {}
|
||||
}
|
||||
|
||||
exports.Statement = Statement;
|
||||
70
langserver/java/statementtypes/SwitchStatement.js
Normal file
70
langserver/java/statementtypes/SwitchStatement.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
|
||||
class SwitchStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {(ResolvedIdent|boolean)[]} */
|
||||
cases = [];
|
||||
/** @type {{cases: (ResolvedIdent|boolean)[], statements: Statement[]} []} */
|
||||
caseBlocks = [];
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
let test_type = null;
|
||||
if (this.test) {
|
||||
test_type = this.test.resolveExpression(vi);
|
||||
if (test_type instanceof NumberLiteral) {
|
||||
test_type = test_type.type;
|
||||
}
|
||||
if (test_type instanceof JavaType) {
|
||||
if (!isTypeAssignable(vi.typemap.get('java/lang/String'), test_type)) {
|
||||
if (!isTypeAssignable(PrimitiveType.map.I, test_type)) {
|
||||
test_type = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
test_type = null;
|
||||
}
|
||||
if (!test_type) {
|
||||
vi.problems.push(ParseProblem.Error(this.test.tokens, `Switch expression must be of type 'int' or 'java.lang.String'`));
|
||||
}
|
||||
}
|
||||
|
||||
vi.statementStack.unshift('switch');
|
||||
|
||||
this.caseBlocks.forEach(caseblock => {
|
||||
caseblock.cases.forEach(c => {
|
||||
if (typeof c === 'boolean') {
|
||||
// default case
|
||||
return;
|
||||
}
|
||||
const case_value = c.resolveExpression(vi);
|
||||
if (case_value instanceof JavaType || case_value instanceof NumberLiteral) {
|
||||
if (test_type && !isTypeAssignable(test_type, case_value)) {
|
||||
const case_type = case_value instanceof JavaType ? case_value : case_value.type;
|
||||
vi.problems.push(ParseProblem.Error(c.tokens, `Incomparable types: expression of type '${case_type.fullyDottedTypeName}' is not comparable with type '${test_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(c.tokens, `Expression expected`));
|
||||
}
|
||||
});
|
||||
caseblock.statements.forEach(statement => statement.validate(vi));
|
||||
})
|
||||
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
exports.SwitchStatement = SwitchStatement;
|
||||
35
langserver/java/statementtypes/SynchronizedStatement.js
Normal file
35
langserver/java/statementtypes/SynchronizedStatement.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { CEIType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class SynchronizedStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.expression) {
|
||||
const value = this.expression.resolveExpression(vi);
|
||||
// locks must be a reference type
|
||||
if (!(value instanceof CEIType)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Lock expression must be a reference type`));
|
||||
}
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('synchronized');
|
||||
this.statement.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.SynchronizedStatement = SynchronizedStatement;
|
||||
32
langserver/java/statementtypes/ThrowStatement.js
Normal file
32
langserver/java/statementtypes/ThrowStatement.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { JavaType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ThrowStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!this.expression) {
|
||||
return;
|
||||
}
|
||||
const throw_value = this.expression.resolveExpression(vi);
|
||||
if (throw_value instanceof JavaType) {
|
||||
if (!isTypeAssignable(vi.typemap.get('java/lang/Throwable'), throw_value)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `throw expression does not inherit from java.lang.Throwable`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Throwable expression expected`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ThrowStatement = ThrowStatement;
|
||||
45
langserver/java/statementtypes/TryStatement.js
Normal file
45
langserver/java/statementtypes/TryStatement.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('./Block').Block} Block
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { ResolvedIdent } = require('../body-types');
|
||||
const { Block } = require('./Block');
|
||||
|
||||
class TryStatement extends KeywordStatement {
|
||||
/** @type {(ResolvedIdent|Local[])[]} */
|
||||
resources = [];
|
||||
/** @type {Block} */
|
||||
block = null;
|
||||
catches = [];
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
this.resources.forEach(r => {
|
||||
if (r instanceof ResolvedIdent) {
|
||||
r.resolveExpression(vi);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.block) {
|
||||
vi.statementStack.unshift('try');
|
||||
this.block.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
|
||||
this.catches.forEach(c => {
|
||||
if (c instanceof Block) {
|
||||
// finally
|
||||
c.validate(vi);
|
||||
} else if (c.block) {
|
||||
// catch block
|
||||
c.block.validate(vi);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.TryStatement = TryStatement;
|
||||
31
langserver/java/statementtypes/WhileStatement.js
Normal file
31
langserver/java/statementtypes/WhileStatement.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
|
||||
class WhileStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.test) {
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('while');
|
||||
this.statement.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.WhileStatement = WhileStatement;
|
||||
269
langserver/java/tokenizer.js
Normal file
269
langserver/java/tokenizer.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').Method} Method
|
||||
* @typedef {import('java-mti').Constructor} Constructor
|
||||
*/
|
||||
const { TextBlock, BlockRange } = require('./parsetypes/textblock');
|
||||
|
||||
/**
|
||||
* Convert a token to its simplified form for easier declaration parsing.
|
||||
*
|
||||
* - Whitespace, comments, strings and character literals are normalised.
|
||||
* - Modifier keywords and identifers are abbreviated.
|
||||
* - Any invalid text is replaced with spaces.
|
||||
*
|
||||
* Abbreviated and normalised values are padded to occupy the same space
|
||||
* as the original text - this ensures any parse errors are reported in the
|
||||
* correct location.
|
||||
* @param {string} text
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
* @param {string} kind
|
||||
*/
|
||||
function tokenKindToSimplified(text, start, length, kind) {
|
||||
const chunk = text.slice(start, start + length);
|
||||
switch (kind) {
|
||||
case 'wsc':
|
||||
return chunk.replace(/[^\r\n]/g, ' ');
|
||||
case 'string-literal':
|
||||
if (chunk.length <= 2) return chunk;
|
||||
return `"${'#'.repeat(chunk.length - 2)}"`;
|
||||
case 'char-literal':
|
||||
if (chunk.length <= 2) return chunk;
|
||||
return `'${'#'.repeat(chunk.length - 2)}'`;
|
||||
case 'primitive-type':
|
||||
return `P${' '.repeat(chunk.length - 1)}`;
|
||||
case 'modifier':
|
||||
return `M${' '.repeat(chunk.length - 1)}`;
|
||||
case 'ident':
|
||||
return `W${' '.repeat(chunk.length - 1)}`;
|
||||
case 'invalid':
|
||||
return ' '.repeat(chunk.length);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
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), tokenKindToSimplified(text, start, length, kind));
|
||||
this.kind = kind;
|
||||
/** @type {{key:string}} */
|
||||
this.loc = null;
|
||||
|
||||
/**
|
||||
* Stores information about the resolved methods/constructors this token is an argument for.
|
||||
* This is used to provide method signature info to vscode
|
||||
* @type {{methods:(Method|Constructor)[], methodIdx:number, argIdx:number}}
|
||||
*/
|
||||
this.methodCallInfo = null;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.source;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* \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
|
||||
* [\p{L}\p{N}_$]* 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]*?\*\/|\/\*[\d\D]*)|("[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*"|".*)|('\\u[\da-fA-F]{0,4}'?|'\\?.?'?)|(\.?\d)|([\p{L}\p{N}$_]+)|(\()|([;,?:(){}\[\]@]|\.(?:\.\.)?)|([!=/%*^]=?|<<?=?|>>?>?=?|&[&=]?|\|[|=]?|(\+\+|--)|->|[+-]=?|~)|$/gu;
|
||||
const raw_token_types = [
|
||||
'wsc',
|
||||
'string-literal',
|
||||
'char-literal',
|
||||
'number-literal',
|
||||
'word',
|
||||
'open-bracket',
|
||||
'symbol',
|
||||
'operator',
|
||||
];
|
||||
/**
|
||||
* Note that some keywords have context-dependant meanings:
|
||||
* default - modifier or statement-keyword
|
||||
* synchronized - modifier or statement-keyword
|
||||
* They are treated as modifiers and updated with their new token-type when method bodies are parsed
|
||||
*
|
||||
* ```
|
||||
* 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|default|synchronized modifier
|
||||
* if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw 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|strictfp|default|synchronized)|(if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw|assert)|(class|enum|interface)|(extends|implements|throws)|(package|import)|(.+))$/;
|
||||
|
||||
const word_token_types = [
|
||||
'boolean-literal',
|
||||
'object-literal',
|
||||
'primitive-type',
|
||||
'new-operator',
|
||||
'instanceof-operator',
|
||||
'modifier',
|
||||
'statement-kw',
|
||||
'type-kw',
|
||||
'package-kw',
|
||||
'eit-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
|
||||
* 0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]? hex exponent: 0x123.abcP-100
|
||||
* 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]?)|(0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]?)|(0[xX][\da-fA-F]*[lL]?)|(\d+[fFdDlL]?)/g;
|
||||
const number_token_types = [
|
||||
'dec-exp-number-literal',
|
||||
'dec-number-literal',
|
||||
'hex-exp-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
|
||||
let word_m = m[0].match(word_re);
|
||||
idx = word_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'|
|
||||
'lambda-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',
|
||||
'lambda-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];
|
||||
}
|
||||
|
||||
|
||||
exports.getOperatorType = getOperatorType;
|
||||
exports.tokenize = tokenize;
|
||||
exports.Token = Token;
|
||||
152
langserver/java/type-resolver.js
Normal file
152
langserver/java/type-resolver.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @typedef {Map<string,CEIType>} TypeMap
|
||||
*/
|
||||
const { JavaType, CEIType, MethodBase, TypeVariable } = require('java-mti');
|
||||
const { ResolvedImport } = require('./import-resolver');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} ident
|
||||
* @param {TypeVariable[]} type_variables
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveTypeOrPackage(ident, type_variables, scope, imports, typemap) {
|
||||
const types = [];
|
||||
let package_name = '';
|
||||
|
||||
const tv = type_variables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
|
||||
if (scope) {
|
||||
|
||||
if (!types[0] && scope instanceof MethodBase) {
|
||||
// is it a type variable in the current scope
|
||||
const tv = scope.typeVariables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
}
|
||||
|
||||
const scoped_type = scope instanceof CEIType ? scope : scope.owner;
|
||||
if (!types[0]) {
|
||||
// is it an enclosed type of the currently scoped type or any outer 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] && scoped_type.simpleTypeName === ident) {
|
||||
types.push(scoped_type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!types[0]) {
|
||||
// is it a type variable of the currently scoped type
|
||||
const tv = scoped_type.typeVariables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!types[0]) {
|
||||
// is it a type from the imports
|
||||
for (let i of imports) {
|
||||
const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(ident) && /[$/]/.test(fqn[fqn.length-ident.length-1]));
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} ident
|
||||
* @param {JavaType[]} outer_types
|
||||
* @param {string} outer_package_name
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveNextTypeOrPackage(ident, outer_types, outer_package_name, typemap) {
|
||||
const types = [];
|
||||
let package_name = '';
|
||||
|
||||
outer_types.forEach(type => {
|
||||
if (type instanceof CEIType) {
|
||||
const enclosed_type_signature = `${type.shortSignature}$${ident}`;
|
||||
const enclosed_type = typemap.get(enclosed_type_signature);
|
||||
if (enclosed_type) {
|
||||
// it matches an inner/enclosed type
|
||||
types.push(enclosed_type);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (outer_package_name) {
|
||||
const { type, sub_package_name } = resolveNextPackage(outer_package_name, ident, typemap);
|
||||
if (type) {
|
||||
types.push(type);
|
||||
}
|
||||
package_name = sub_package_name;
|
||||
}
|
||||
|
||||
return {
|
||||
types,
|
||||
package_name,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} package_name
|
||||
* @param {string} ident
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveNextPackage(package_name, ident, typemap) {
|
||||
let type = null, sub_package_name = '';
|
||||
const qualified_name = `${package_name}/${ident}`;
|
||||
type = typemap.get(qualified_name) || null;
|
||||
const package_match = qualified_name + '/';
|
||||
if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) {
|
||||
// it matches a sub-package
|
||||
sub_package_name = qualified_name;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
sub_package_name
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveTypeOrPackage,
|
||||
resolveNextTypeOrPackage,
|
||||
resolveNextPackage,
|
||||
}
|
||||
169
langserver/java/typeident.js
Normal file
169
langserver/java/typeident.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const { ArrayType, CEIType, JavaType, PrimitiveType, MethodBase, WildcardType, TypeVariable } = require('java-mti');
|
||||
const { SourceTypeIdent, SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
|
||||
const ResolvedImport = require('./parsetypes/resolved-import');
|
||||
const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver');
|
||||
const { Token } = require('./tokenizer');
|
||||
const { AnyType } = require("./anys");
|
||||
|
||||
/**
|
||||
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMC
|
||||
* @typedef {import('./TokenList').TokenList} TokenList
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function typeIdentList(tokens, scope, imports, typemap) {
|
||||
let type = typeIdent(tokens, scope, imports, typemap);
|
||||
const types = [type];
|
||||
while (tokens.current.value === ',') {
|
||||
tokens.inc();
|
||||
type = typeIdent(tokens, scope, imports, typemap);
|
||||
types.push(type);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {{no_array_qualifiers:boolean, type_vars:TypeVariable[]}} [opts]
|
||||
*/
|
||||
function typeIdent(tokens, scope, imports, typemap, opts) {
|
||||
tokens.mark();
|
||||
const type = singleTypeIdent(tokens, scope, imports, typemap, opts);
|
||||
return new SourceTypeIdent(tokens.markEnd(), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {{no_array_qualifiers:boolean, type_vars: TypeVariable[]}} [opts]
|
||||
*/
|
||||
function singleTypeIdent(tokens, scope, imports, typemap, opts) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [], package_name = '';
|
||||
tokens.mark();
|
||||
switch(tokens.current.kind) {
|
||||
case 'ident':
|
||||
({ types, package_name } = resolveTypeOrPackage(tokens.current.value, opts ? opts.type_vars : [], scope, imports, typemap));
|
||||
break;
|
||||
case 'primitive-type':
|
||||
types.push(PrimitiveType.fromName(tokens.current.value));
|
||||
break;
|
||||
default:
|
||||
return tokens.current.value === '?'
|
||||
? wildcardTypeArgument(tokens, scope, imports, typemap)
|
||||
: AnyType.Instance;
|
||||
}
|
||||
tokens.inc();
|
||||
for (;;) {
|
||||
if (tokens.isValue('.')) {
|
||||
if (tokens.current.kind !== 'ident') {
|
||||
break;
|
||||
}
|
||||
({ types, package_name } = resolveNextTypeOrPackage(tokens.current.value, types, package_name, typemap));
|
||||
tokens.inc();
|
||||
} else if (tokens.isValue('<')) {
|
||||
genericTypeArgs(tokens, types, scope, imports, typemap);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const type_tokens = tokens.markEnd();
|
||||
if (!types[0]) {
|
||||
const anytype = new AnyType(type_tokens.map(t => t.source).join(''));
|
||||
types.push(anytype);
|
||||
}
|
||||
|
||||
// allow array qualifiers unless specifically disabled
|
||||
const allow_array_qualifiers = !opts || !opts.no_array_qualifiers;
|
||||
if ( allow_array_qualifiers && tokens.isValue('[')) {
|
||||
let arrdims = 0;
|
||||
for(;;) {
|
||||
arrdims++;
|
||||
tokens.expectValue(']');
|
||||
if (!tokens.isValue('[')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
types = types.map(t => new ArrayType(t, arrdims));
|
||||
}
|
||||
|
||||
return types[0];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TokenList} tokens
|
||||
* @param {JavaType[]} types
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function genericTypeArgs(tokens, types, scope, imports, typemap) {
|
||||
if (tokens.isValue('>')) {
|
||||
// <> operator - build new types with inferred type arguments
|
||||
types.forEach((t,i,arr) => {
|
||||
if (t instanceof CEIType) {
|
||||
let specialised = t.makeInferredTypeArgs();
|
||||
arr[i] = specialised;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
const type_arguments = typeIdentList(tokens, scope, imports, typemap).map(s => s.resolved);
|
||||
types.forEach((t,i,arr) => {
|
||||
if (t instanceof CEIType) {
|
||||
let specialised = t.specialise(type_arguments);
|
||||
if (typemap.has(specialised.shortSignature)) {
|
||||
arr[i] = typemap.get(specialised.shortSignature);
|
||||
return;
|
||||
}
|
||||
typemap.set(specialised.shortSignature, specialised);
|
||||
arr[i] = specialised;
|
||||
}
|
||||
});
|
||||
if (/>>>?/.test(tokens.current.value)) {
|
||||
// we need to split >> and >>> into separate > tokens to handle things like List<Class<?>>
|
||||
const new_tokens = tokens.current.value.split('').map((gt,i) => new Token(tokens.current.range.source, tokens.current.range.start + i, 1, 'comparison-operator'));
|
||||
tokens.splice(tokens.idx, 1, ...new_tokens);
|
||||
}
|
||||
tokens.expectValue('>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @returns {WildcardType}
|
||||
*/
|
||||
function wildcardTypeArgument(tokens, scope, imports, typemap) {
|
||||
tokens.expectValue('?');
|
||||
let bound = null;
|
||||
switch (tokens.current.value) {
|
||||
case 'extends':
|
||||
case 'super':
|
||||
const kind = tokens.current.value;
|
||||
tokens.inc();
|
||||
bound = {
|
||||
kind,
|
||||
type: singleTypeIdent(tokens, scope, imports, typemap),
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new WildcardType(bound);
|
||||
}
|
||||
|
||||
exports.typeIdent = typeIdent;
|
||||
exports.typeIdentList = typeIdentList;
|
||||
exports.genericTypeArgs = genericTypeArgs;
|
||||
65
langserver/java/validater.js
Normal file
65
langserver/java/validater.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { CEIType } = require('java-mti');
|
||||
const { resolveImports } = require('../java/import-resolver');
|
||||
const { SourceUnit } = require('./source-types');
|
||||
const { parseTypeMethods } = require('./body-parser');
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
* @param {Map<string, CEIType>} typemap
|
||||
*/
|
||||
function parseMethodBodies(unit, typemap) {
|
||||
const resolved_types = [
|
||||
...resolveImports(typemap, unit.packageName),
|
||||
...unit.imports.filter(i => i.resolved).map(i => i.resolved),
|
||||
]
|
||||
unit.types.forEach(t => parseTypeMethods(t, resolved_types, typemap));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
* @param {Map<string, CEIType>} androidLibrary
|
||||
* @returns {import('./parsetypes/parse-problem')[]}
|
||||
*/
|
||||
function validate(unit, androidLibrary) {
|
||||
let probs = [];
|
||||
|
||||
const module_validaters = [
|
||||
// require('./validation/multiple-package-decls'),
|
||||
// require('./validation/unit-decl-order'),
|
||||
// require('./validation/duplicate-members'),
|
||||
// require('./validation/parse-errors'),
|
||||
// require('./validation/modifier-errors'),
|
||||
// require('./validation/unresolved-imports'),
|
||||
// require('./validation/invalid-types'),
|
||||
// require('./validation/bad-extends'),
|
||||
// require('./validation/bad-implements'),
|
||||
// require('./validation/non-implemented-interfaces'),
|
||||
// require('./validation/bad-overrides'),
|
||||
// require('./validation/missing-constructor'),
|
||||
//require('./validation/expression-compatibility'),
|
||||
];
|
||||
let problems = [
|
||||
module_validaters.map(v => v(unit.types, unit)),
|
||||
...probs,
|
||||
];
|
||||
|
||||
function flatten(arr) {
|
||||
let res = arr;
|
||||
for (;;) {
|
||||
const idx = res.findIndex(x => Array.isArray(x));
|
||||
if (idx < 0) {
|
||||
return res;
|
||||
}
|
||||
res = [...res.slice(0, idx), ...res[idx], ...res.slice(idx+1)]
|
||||
}
|
||||
}
|
||||
|
||||
let flattened = flatten(problems).filter(x => x);
|
||||
console.log(`Problems: ${flattened.length}`)
|
||||
return flattened;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validate,
|
||||
parseMethodBodies,
|
||||
}
|
||||
55
langserver/java/validation/bad-extends.js
Normal file
55
langserver/java/validation/bad-extends.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { SourceType } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkExtends(source_type, probs) {
|
||||
const supertypes = source_type.extends_types
|
||||
.map(st => st.resolved)
|
||||
.filter(t => !(t instanceof AnyType));
|
||||
|
||||
if (supertypes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const supertype = supertypes[0];
|
||||
if (source_type.typeKind === 'enum') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Enum types cannot declare a superclass`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertypes.length > 1) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[1].tokens, `Class types cannot inherit from more than one type`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype.typeKind !== 'class') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype.typeKind === 'class' && supertype.modifiers.includes('final')) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from final class: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype === source_type) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from itself`));
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
supertypes.forEach((supertype, i) => {
|
||||
if (supertype.typeKind !== 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (supertype === source_type) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from itself`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkExtends(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
43
langserver/java/validation/bad-implements.js
Normal file
43
langserver/java/validation/bad-implements.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const {SourceType} = require('../source-types');
|
||||
const { AnyType } = require('../anys');
|
||||
const { UnresolvedType } = require('java-mti');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkImplements(source_type, probs) {
|
||||
const superinterfaces = source_type.implements_types
|
||||
.map(st => st.resolved)
|
||||
.filter(t => !(t instanceof AnyType));
|
||||
|
||||
if (superinterfaces.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.implements_types[0].tokens, `Interface types cannot declare an implements section`));
|
||||
}
|
||||
if (source_type.typeKind === 'class') {
|
||||
superinterfaces.forEach((intf, i) => {
|
||||
if (intf instanceof UnresolvedType) {
|
||||
return;
|
||||
}
|
||||
if (intf.typeKind !== 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.implements_types[i].tokens, `Class '${source_type.fullyDottedRawName}' cannot implement ${intf.typeKind} type: '${intf.fullyDottedRawName}'`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkImplements(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
65
langserver/java/validation/bad-overrides.js
Normal file
65
langserver/java/validation/bad-overrides.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const {SourceType, SourceAnnotation} = require('../source-types');
|
||||
const {CEIType, Method} = require('java-mti');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkOverrides(source_type, probs) {
|
||||
if (source_type.extends_types.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind !== 'class') {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {{ann:SourceAnnotation, method:Method, method_id:string}[]} */
|
||||
const overriden_methods = [];
|
||||
source_type.sourceMethods.reduce((arr, method) => {
|
||||
const ann = method.annotations.find(a => a.type.simpleTypeName === 'Override');
|
||||
if (ann) {
|
||||
arr.push({
|
||||
ann,
|
||||
method,
|
||||
method_id: `${method.name}${method.methodSignature}`,
|
||||
})
|
||||
}
|
||||
return arr;
|
||||
}, overriden_methods);
|
||||
|
||||
if (!overriden_methods.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methods = new Set(), supers_done = new Set();
|
||||
const supers = source_type.supers.slice();
|
||||
while (supers.length) {
|
||||
const s = supers.shift();
|
||||
supers_done.add(s);
|
||||
s.methods.forEach(m => {
|
||||
methods.add(`${m.name}${m.methodSignature}`);
|
||||
});
|
||||
if (s instanceof CEIType) {
|
||||
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
|
||||
}
|
||||
}
|
||||
|
||||
overriden_methods.forEach(x => {
|
||||
if (!methods.has(x.method_id)) {
|
||||
probs.push(ParseProblem.Error(x.ann.annotationTypeIdent.tokens, `${x.method.label} does not override a matching method in any inherited type or interface`));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkOverrides(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
53
langserver/java/validation/invalid-types.js
Normal file
53
langserver/java/validation/invalid-types.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { SourceType, SourceTypeIdent } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceTypeIdent} type
|
||||
* @param {boolean} is_return_type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkType(type, is_return_type, probs) {
|
||||
const typesig = type.resolved.typeSignature;
|
||||
if (/^\[*U/.test(typesig)) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `Unresolved type '${type.resolved.label}'`))
|
||||
return;
|
||||
}
|
||||
if (typesig === 'V' && !is_return_type) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `'void' is not a valid type for variables`))
|
||||
}
|
||||
if (/^\[+V/.test(typesig)) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `Illegal type: '${type.resolved.label}'`))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkInvalidTypes(type, probs) {
|
||||
type.fields.forEach(f => checkType(f.fieldTypeIdent, false, probs));
|
||||
type.sourceMethods.forEach(m => {
|
||||
checkType(m.returnTypeIdent, true, probs);
|
||||
m.parameters.forEach(p => {
|
||||
checkType(p.paramTypeIdent, false, probs);
|
||||
})
|
||||
})
|
||||
type.constructors.forEach(c => {
|
||||
c.parameters.forEach(p => {
|
||||
checkType(p.paramTypeIdent, false, probs);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkInvalidTypes(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
40
langserver/java/validation/missing-constructor.js
Normal file
40
langserver/java/validation/missing-constructor.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const {SourceType, SourceConstructor} = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConstructor(source_type, probs) {
|
||||
if (source_type.typeKind !== 'class') {
|
||||
return;
|
||||
}
|
||||
if (source_type.constructors[0] instanceof SourceConstructor) {
|
||||
return;
|
||||
}
|
||||
const superclass = source_type.supers.find(s => s.typeKind === 'class');
|
||||
if (!superclass) {
|
||||
// if there's no superclass, the class must inherit from an interface
|
||||
// - which means the inherited class is Object (and a default constructor exists)
|
||||
return;
|
||||
}
|
||||
if (superclass.constructors.length) {
|
||||
if (!superclass.constructors.find(c => c.parameterCount === 0)) {
|
||||
// the source type has no declared constructors, but the superclass
|
||||
// does not include a default (parameterless) constructor
|
||||
probs.push(ParseProblem.Error(source_type.nameToken, `Class '${source_type.fullyDottedRawName}' requires a constructor to be declared because the inherited class '${superclass.fullyDottedRawName}' does not define a default constructor.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkConstructor(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
188
langserver/java/validation/modifier-errors.js
Normal file
188
langserver/java/validation/modifier-errors.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const { SourceType, SourceMethod, SourceParameter, SourceField, SourceConstructor, SourceInitialiser } = require('../source-types');
|
||||
const { Token } = require('../tokenizer');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkDuplicate(mods, probs) {
|
||||
if (mods.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const m = new Map();
|
||||
for (let mod of mods) {
|
||||
const firstmod = m.get(mod.source);
|
||||
if (firstmod === undefined) {
|
||||
m.set(mod.source, mod);
|
||||
} else {
|
||||
probs.push(ParseProblem.Error(mod, 'Duplicate modifier'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConflictingAccess(mods, probs) {
|
||||
if (mods.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const allmods = mods.map(m => m.source).join(' ');
|
||||
for (let mod of mods) {
|
||||
let match;
|
||||
switch (mod.source) {
|
||||
case 'private':
|
||||
match = allmods.match(/protected|public/);
|
||||
break;
|
||||
case 'protected':
|
||||
match = allmods.match(/private|public/);
|
||||
break;
|
||||
case 'public':
|
||||
match = allmods.match(/private|protected/);
|
||||
break;
|
||||
}
|
||||
if (match) {
|
||||
probs.push(ParseProblem.Error(mod, `Access modifier '${mod.source}' conflicts with '${match[0]}'`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceField} field
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkFieldModifiers(field, probs) {
|
||||
checkDuplicate(field.modifierTokens, probs);
|
||||
checkConflictingAccess(field.modifierTokens, probs);
|
||||
for (let mod of field.modifierTokens) {
|
||||
switch (mod.source) {
|
||||
case 'abstract':
|
||||
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be abstract'));
|
||||
break;
|
||||
case 'native':
|
||||
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be native'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceParameter} param
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkParameterModifiers(param, probs) {
|
||||
// the only permitted modifier is final
|
||||
let has_final = false;
|
||||
param.modifierTokens.forEach(mod => {
|
||||
if (mod.value === 'final') {
|
||||
if (has_final) {
|
||||
probs.push(ParseProblem.Error(mod, `Repeated modifier: final`));
|
||||
}
|
||||
has_final = true;
|
||||
return;
|
||||
}
|
||||
probs.push(ParseProblem.Error(mod, `Parameter declarations cannot be ${mod.value}`));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {Map<string,*>} ownertypemods
|
||||
* @param {SourceMethod} method
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkMethodModifiers(type, ownertypemods, method, probs) {
|
||||
checkDuplicate(method.modifierTokens, probs);
|
||||
checkConflictingAccess(method.modifierTokens, probs);
|
||||
|
||||
method.parameters.forEach(p => checkParameterModifiers(p, probs));
|
||||
|
||||
const allmods = new Map(method.modifierTokens.map(m => [m.source, m]));
|
||||
const is_interface_kind = /@?interface/.test(type.typeKind);
|
||||
const has_body = method.hasImplementation;
|
||||
|
||||
if (allmods.has('abstract') && allmods.has('final')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and final'));
|
||||
}
|
||||
if (allmods.has('abstract') && allmods.has('native')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and native'));
|
||||
}
|
||||
if (allmods.has('abstract') && has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations marked as abstract cannot have a method body'));
|
||||
}
|
||||
if (!is_interface_kind && !allmods.has('abstract') && !allmods.has('native') && !has_body) {
|
||||
probs.push(ParseProblem.Error(method.nameToken, `Method '${method.name}' must have an implementation or be defined as abstract or native`));
|
||||
}
|
||||
if (!is_interface_kind && allmods.has('abstract') && !ownertypemods.has('abstract')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), `Method '${method.name}' cannot be declared abstract inside a non-abstract type`));
|
||||
}
|
||||
if (is_interface_kind && has_body && !allmods.has('default')) {
|
||||
probs.push(ParseProblem.Error(method.body[0], `Non-default interface methods cannot have a method body`));
|
||||
}
|
||||
if (allmods.has('native') && has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('native'), 'Method declarations marked as native cannot have a method body'));
|
||||
}
|
||||
// JLS8
|
||||
if (type.typeKind !== 'interface' && allmods.has('default')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations are only allowed inside interfaces`));
|
||||
}
|
||||
if (allmods.has('default') && !has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations must have an implementation`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceConstructor} field
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConstructorModifiers(field, probs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceInitialiser} initialiser
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkInitialiserModifiers(initialiser, probs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkTypeModifiers(type, probs) {
|
||||
const typemods = new Map(type.modifierTokens.map(m => [m.source, m]));
|
||||
checkDuplicate(type.modifierTokens, probs);
|
||||
|
||||
if (type.typeKind === 'interface' && typemods.has('final')) {
|
||||
probs.push(ParseProblem.Error(typemods.get('final'), 'Interface declarations cannot be marked as final'));
|
||||
}
|
||||
if (type.typeKind === 'enum' && typemods.has('abstract')) {
|
||||
probs.push(ParseProblem.Error(typemods.get('abstract'), 'Enum declarations cannot be marked as abstract'));
|
||||
}
|
||||
if (/[$]/.test(type._rawShortSignature)) {
|
||||
checkConflictingAccess(type.modifierTokens, probs);
|
||||
} else {
|
||||
// top-level types cannot be private, protected or static
|
||||
for (let mod of ['private','protected', 'static']) {
|
||||
if (typemods.has(mod)) {
|
||||
probs.push(ParseProblem.Error(typemods.get(mod), `Top-level declarations cannot be marked as ${mod}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type.fields.forEach(field => checkFieldModifiers(field, probs));
|
||||
type.sourceMethods.forEach(method => checkMethodModifiers(type, typemods, method, probs));
|
||||
type.constructors.forEach(ctr => checkConstructorModifiers(ctr, probs));
|
||||
type.initers.forEach(initer => checkInitialiserModifiers(initer, probs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} types
|
||||
*/
|
||||
module.exports = function(types) {
|
||||
const probs = [];
|
||||
types.forEach(type => checkTypeModifiers(type, probs));
|
||||
return probs;
|
||||
}
|
||||
85
langserver/java/validation/non-implemented-interfaces.js
Normal file
85
langserver/java/validation/non-implemented-interfaces.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { SourceType } = require('../source-types');
|
||||
const { CEIType, Method} = require('java-mti');
|
||||
const {isTypeAssignable} = require('../expression-resolver');
|
||||
|
||||
function nonAbstractLabel(label) {
|
||||
return label.replace(/\babstract /g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Method} impl method implementation
|
||||
* @param {Method} method interface method
|
||||
*/
|
||||
function isMethodCompatible(impl, method) {
|
||||
const impl_params = impl.parameters;
|
||||
const method_params = method.parameters;
|
||||
if (impl_params.length !== method_params.length) {
|
||||
return false;
|
||||
}
|
||||
return impl_params.every((p,idx) => isTypeAssignable(method_params[idx].type, p.type))
|
||||
&& isTypeAssignable(method.returnType, impl.returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkImplementedInterfaces(source_type, probs) {
|
||||
if (source_type.implements_types.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
return;
|
||||
}
|
||||
if (source_type.modifiers.includes('abstract')) {
|
||||
return;
|
||||
}
|
||||
/** @type {Set<CEIType>} */
|
||||
const interfaces = new Set(), supers_done = new Set();
|
||||
const supers = source_type.supers.slice();
|
||||
while (supers.length) {
|
||||
const s = supers.shift();
|
||||
supers_done.add(s);
|
||||
if (s instanceof CEIType) {
|
||||
if (s.typeKind === 'interface') {
|
||||
interfaces.add(s);
|
||||
}
|
||||
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
|
||||
}
|
||||
}
|
||||
|
||||
const implemented = source_type.methods.map(m => `${m.name}${m.methodSignature}`);
|
||||
interfaces.forEach((intf, i) => {
|
||||
const missing_methods = [];
|
||||
intf.methods.forEach(m => {
|
||||
// default methods don't require implementing
|
||||
if (m.hasImplementation) {
|
||||
return;
|
||||
}
|
||||
const namedsig = `${m.name}${m.methodSignature}`
|
||||
if (implemented.indexOf(namedsig) < 0) {
|
||||
// perform a more detailed search for a compatible match
|
||||
if (!source_type.methods.find(source_method => source_method.name === m.name && isMethodCompatible(source_method, m))) {
|
||||
missing_methods.push(nonAbstractLabel(m.label));
|
||||
}
|
||||
}
|
||||
})
|
||||
if (missing_methods.length) {
|
||||
probs.push(ParseProblem.Error(source_type.kind_token, `Non-abstract ${source_type.typeKind} '${source_type.fullyDottedRawName}' does not implement the following methods from interface '${intf.fullyDottedTypeName}':\n${missing_methods.join('\n')}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkImplementedInterfaces(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
17
langserver/java/validation/unresolved-imports.js
Normal file
17
langserver/java/validation/unresolved-imports.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { SourceUnit } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
*/
|
||||
module.exports = function(mod, unit) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
unit.imports.forEach(i => {
|
||||
if (!i.resolved)
|
||||
probs.push(ParseProblem.Warning(i.nameTokens, `Unresolved import: ${i.package_name}`));
|
||||
})
|
||||
|
||||
return probs;
|
||||
}
|
||||
Reference in New Issue
Block a user