From 1b1202598c08e346ccdf04231acf115000674dcf Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Mon, 8 Jun 2020 15:52:03 +0100 Subject: [PATCH] refactor to prepare for merging with type parsing --- langserver/java/TokenList.js | 72 ++++ langserver/java/body-parser3.js | 565 +------------------------------ langserver/java/body-types.js | 356 +++++++++++++++++++ langserver/java/type-resolver.js | 99 ++++++ langserver/java/typeident.js | 94 +++++ 5 files changed, 629 insertions(+), 557 deletions(-) create mode 100644 langserver/java/TokenList.js create mode 100644 langserver/java/body-types.js create mode 100644 langserver/java/typeident.js diff --git a/langserver/java/TokenList.js b/langserver/java/TokenList.js new file mode 100644 index 0000000..e69eeae --- /dev/null +++ b/langserver/java/TokenList.js @@ -0,0 +1,72 @@ +/** + * @typedef {import('./tokenizer').Token} Token + */ +const ParseProblem = require('./parsetypes/parse-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 = []; + } + + inc() { + for (; ;) { + this.current = this.tokens[this.idx += 1]; + if (!this.current || this.current.kind !== 'wsc') { + return this.current; + } + } + } + /** + * Check if the current token matches the specified value and consumes it + * @param {string} value + */ + isValue(value) { + if (this.current.value === value) { + this.inc(); + return true; + } + return false; + } + + /** + * Check if the current token matches the specified value and consumes it or reports an error + * @param {string} value + */ + expectValue(value) { + if (this.isValue(value)) { + return true; + } + const addproblem = require("./body-parser3").addproblem; + addproblem(this, ParseProblem.Error(this.current, `${value} expected`)); + return false; + } + + get previous() { + for (let idx = this.idx - 1; idx >= 0; idx--) { + if (idx === 0 || this.tokens[idx].kind !== 'wsc') { + return this.tokens[idx]; + } + } + } + + /** + * @param {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; diff --git a/langserver/java/body-parser3.js b/langserver/java/body-parser3.js index 48309ba..f26e1cc 100644 --- a/langserver/java/body-parser3.js +++ b/langserver/java/body-parser3.js @@ -9,6 +9,10 @@ const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source const ResolvedImport = require('./parsetypes/resolved-import'); const ParseProblem = require('./parsetypes/parse-problem'); const { getOperatorType, Token } = require('./tokenizer'); +const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver'); +const { typeIdentList } = require('./typeident'); +const { TokenList } = require("./TokenList"); +const { AnyMethod, AnyType, AnyValue, ArrayElement, ArrayLiteral, ConstructorCall, LiteralNumber, LiteralValue, Local, MethodCall, ResolvedIdent, TernaryValue, Value } = require("./body-types"); /** * @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMC @@ -77,67 +81,6 @@ function addLocals(tokens, locals, new_locals) { } } -class TokenList { - /** - * @param {Token[]} tokens - */ - constructor(tokens) { - this.tokens = tokens; - this.idx = -1; - /** @type {Token} */ - this.current = null; - this.inc(); - /** @type {ParseProblem[]} */ - this.problems = []; - } - - inc() { - for (;;) { - this.current = this.tokens[this.idx += 1]; - if (!this.current || this.current.kind !== 'wsc') { - return this.current; - } - } - } - - /** - * Check if the current token matches the specified value and consumes it - * @param {string} value - */ - isValue(value) { - if (this.current.value === value) { - this.inc(); - return true; - } - return false; - } - - /** - * Check if the current token matches the specified value and consumes it or reports an error - * @param {string} value - */ - expectValue(value) { - if (this.isValue(value)) { - return true; - } - addproblem(this, ParseProblem.Error(this.current, `${value} expected`)); - return false; - } - - get previous() { - for (let idx = this.idx-1; idx >= 0 ; idx--) { - if (idx === 0 || this.tokens[idx].kind !== 'wsc') { - return this.tokens[idx]; - } - } - } - - splice(start, delete_count, ...add) { - this.tokens.splice(start, delete_count, ...add); - this.current = this.tokens[this.idx]; - } -} - /** * @param {TokenList} tokens * @param {Local[]} locals @@ -1774,82 +1717,6 @@ function expressionList(tokens, locals, method, imports, typemap) { return expressions; } -/** - * @param {TokenList} tokens - * @param {SourceMC} method - * @param {ResolvedImport[]} imports - * @param {Map} typemap - */ -function typeIdentList(tokens, method, imports, typemap) { - let type = typeIdent(tokens, method, imports, typemap); - const types = [type]; - while (tokens.current.value === ',') { - tokens.inc(); - type = typeIdent(tokens, method, imports, typemap); - types.push(type); - } - return types; -} - -/** - * @param {TokenList} tokens - * @param {SourceMC} method - * @param {ResolvedImport[]} imports - * @param {Map} typemap - */ -function typeIdent(tokens, method, imports, typemap) { - if (tokens.current.kind !== 'ident') { - if (tokens.current.value === '?') { - return wildcardTypeArgument(tokens, method, imports, typemap); - } - return AnyType.Instance; - } - const { types, package_name } = resolveTypeOrPackage(tokens.current.value, method._owner, imports, typemap); - let matches = new ResolvedIdent(tokens.current.value, [], [], types, package_name); - tokens.inc(); - for (;;) { - if (tokens.isValue('.')) { - matches = parseDottedIdent(matches, tokens, typemap); - } else if (tokens.isValue('<')) { - if (!tokens.isValue('>')) { - typeIdentList(tokens, method, imports, typemap); - if (/>>>?/.test(tokens.current.value)) { - // we need to split >> and >>> into separate > tokens to handle things like List> - 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('>'); - } - } else { - break; - } - } - return matches.types[0] || new UnresolvedType(matches.source); -} - -/** - * @param {TokenList} tokens - * @param {SourceMC} method - * @param {ResolvedImport[]} imports - * @param {Map} typemap - */ -function wildcardTypeArgument(tokens, method, 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: typeIdent(tokens, method, imports, typemap), - } - break; - } - return new WildcardType(bound); -} - /** * @param {TokenList} tokens * @param {Local[]} locals @@ -2136,41 +2003,22 @@ function parseDottedIdent(matches, tokens, typemap) { matches.types.forEach(t => { // if there is an AnyType, then add a type, variable and method // - this prevents multiple errors in dotted values/ - // e.g R.layout.name wiil only error once (on R), not on all 3 idents + // e.g R.layout.name will only error once (on R), not on all 3 idents if (t instanceof AnyType) { types.push(new AnyType(qualified_ident)); variables.push(new AnyValue(qualified_ident)); methods.push(new AnyMethod(tokens.current.value)); return; } - if (t instanceof CEIType) { - const enclosed_type_signature = `${t.shortSignature}$${tokens.current.value}`; - const enc_type = typemap.get(enclosed_type_signature); - if (enc_type) { - types.push(enc_type); - } - } // search static fields and methods const decls = t.findDeclsByName(tokens.current.value); variables.push(...decls.fields); methods.push(...decls.methods); }); - if (matches.package_name) { - // if there is a package name, the ident could represent a sub-package or a top-leve type name - const type_match = `${matches.package_name}/${tokens.current.value}`; - if (typemap.has(type_match)) { - // it matches a type - types.push(typemap.get(type_match)); - } else { - const package_match = `${matches.package_name}/${tokens.current.value}/`; - if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) { - package_name = type_match; - } - } - } + const members = resolveNextTypeOrPackage(tokens.current.value, matches.types, matches.package_name, typemap); - const match = new ResolvedIdent(qualified_ident, variables, methods, types, package_name); + const match = new ResolvedIdent(qualified_ident, variables, methods, [...types, ...members.types ], members.package_name); checkIdentifierFound(tokens, tokens.current.value, match); tokens.inc(); return match; @@ -2271,403 +2119,6 @@ function findIdentifier(ident, locals, method, imports, typemap) { return matches; } -/** - * - * @param {string} ident - * @param {CEIType} scoped_type - * @param {ResolvedImport[]} imports - * @param {Map} typemap - */ -function resolveTypeOrPackage(ident, scoped_type, imports, typemap) { - const types = []; - let package_name = ''; - - // is it an enclosed type of the currently scoped type or any outer type - if (scoped_type) { - const scopes = scoped_type.shortSignature.split('$'); - while (scopes.length) { - const enc_type = typemap.get(`${scopes.join('$')}$${ident}`); - if (enc_type) { - types.push(enc_type); - break; - } - scopes.pop(); - } - } - - if (!types[0]) { - // is it a top-level type from the imports - const top_level_type = '/' + ident; - for (let i of imports) { - const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(top_level_type)); - if (fqn) { - types.push(i.types.get(fqn)); - } - } - } - - if (!types[0]) { - // is it a default-package type - const default_type = typemap.get(ident); - if (default_type) { - types.push(default_type); - } - } - - // the final option is the start of a package name - const package_root = ident + '/'; - const typelist = [...typemap.keys()]; - if (typelist.find(fqn => fqn.startsWith(package_root))) { - package_name = ident; - } - - return { - types, - package_name, - } -} - - -/** - * AnyType is a special type that's used to fill in types that are missing. - * To prevent cascading errors, AnyType should be fully assign/cast/type-compatible - * with any other type - */ -class AnyType extends JavaType { - /** - * - * @param {String} label - */ - constructor(label) { - super("class", [], ''); - super.simpleTypeName = label; - } - - static Instance = new AnyType(''); - - get rawTypeSignature() { - return 'U'; - } - - get typeSignature() { - return 'U'; - } -} - -class AnyMethod extends Method { - /** - * @param {string} name - */ - constructor(name) { - super(name, [], ''); - } - - get returnType() { - return AnyType.Instance; - } -} - -class Local { - /** - * @param {Token[]} modifiers - * @param {string} name - * @param {Token} decltoken - * @param {JavaType} type - * @param {JavaType} type - * @param {number} postnamearrdims - */ - constructor(modifiers, name, decltoken, type, postnamearrdims) { - this.finalToken = modifiers.find(m => m.source === 'final') || null; - this.name = name; - this.decltoken = decltoken; - if (postnamearrdims > 0) { - this.type = (type instanceof ArrayType) - ? new ArrayType(type.base, type.arrdims + postnamearrdims) - : new ArrayType(type, postnamearrdims); - } else { - this.type = type; - } - this.init = null; - } -} - -class ArrayElement { - /** - * - * @param {Local|Parameter|Field|ArrayElement|Value} array_variable - * @param {ResolvedIdent} index - */ - constructor(array_variable, index) { - this.array_variable = array_variable; - this.index = index; - if (!(this.array_variable.type instanceof ArrayType)) { - throw new Error('Array element cannot be created from non-array type'); - } - this.name = `${array_variable.name}[${index.source}]`; - /** @type {JavaType} */ - this.type = this.array_variable.type.elementType; - } -} - -class Value { - /** - * @param {string} name - * @param {JavaType} type - */ - constructor(name, type) { - this.name = name; - this.type = type; - } - - /** - * @param {string} ident - * @param {ResolvedIdent} lhs - * @param {ResolvedIdent} rhs - * @param {JavaType} type - */ - static build(ident, lhs, rhs, type) { - if (!lhs.variables[0] || !rhs.variables[0]) { - return new Value(ident, type); - } - if (lhs.variables[0] instanceof LiteralValue && rhs.variables && rhs.variables[0] instanceof LiteralValue) { - new LiteralValue(ident, type); - } - return new Value(ident, type); - } -} - -class AnyValue extends Value { - constructor(name) { - super(name, AnyType.Instance); - } -} - -class LiteralValue extends Value { } - -/** - * LiteralNumberType is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc). - * - * It's used to allow literal numbers to be type-assignable to variables with different primitive types. - * For example, 200 is type-assignable to short, int, long, float and double, but not byte. - */ -class LiteralNumber extends LiteralValue { - /** - * @param {string} value - * @param {string} kind - * @param {PrimitiveType} default_type - */ - constructor(value, kind, default_type) { - super(value, default_type); - this.numberValue = value; - this.numberKind = kind; - } - - static shift(a, b, op) { - const ai = a.toInt(), bi = b.toInt(); - if (ai === null || bi === null) { - return null; - } - const val = op(ai, bi); - const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I; - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static bitwise(a, b, op) { - const ai = a.toInt(), bi = b.toInt(); - if (ai === null || bi === null) { - return null; - } - const val = op(ai, bi); - const typekey = a.type.typeSignature+ b.type.typeSignature; - let type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I; - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static math(a, b, op, divmod) { - const ai = a.toNumber(), bi = b.toNumber(); - if (bi === 0 && divmod) { - return null; - } - let val = op(ai, bi); - const typekey = a.type.typeSignature+ b.type.typeSignature; - if (!/[FD]/.test(typekey) && divmod) { - val = Math.trunc(val); - } - let type; - if (/^(D|F[^D]|J[^FD])/.test(typekey)) { - type = a.type; - } else { - type = b.type; - } - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static '+'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a + b) } - static '-'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a - b) } - static '*'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a * b) } - static '/'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a / b, true) } - static '%'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a % b, true) } - static '&'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a & b) } - static '|'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a | b) } - static '^'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a ^ b) } - static '>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a >> b) } - static '>>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => { - // unsigned shift (>>>) is not supported by bigints - // @ts-ignore - return (a >> b) & ~(-1n << (64n - b)); - }) } - static '<<'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a << b) } - - toInt() { - switch (this.numberKind) { - case 'hex-number-literal': - case 'int-number-literal': - // unlike parseInt, BigInt doesn't like invalid characters, so - // ensure we strip any trailing long specifier - return BigInt(this.name.match(/(.+?)[lL]?$/)[1]); - } - return null; - } - - toNumber() { - return parseFloat(this.name); - } - - /** - * @param {JavaType} type - */ - isCompatibleWith(type) { - if (this.type === type) { - return true; - } - switch(this.type.simpleTypeName) { - case 'double': - return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature); - case 'float': - return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature); - } - // all integral types are all compatible with long, float and double variables - if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) { - return true; - } - // the desintation type must be a number primitive or one of the corresponding boxed classes - if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) { - return false; - } - let number = 0; - if (this.numberKind === 'hex-number-literal') { - if (this.numberValue !== '0x') { - const non_leading_zero_digits = this.numberValue.match(/0x0*(.+)/)[1]; - number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16); - } - } else if (this.numberKind === 'int-number-literal') { - const non_leading_zero_digits = this.numberValue.match(/0*(.+)/)[1]; - number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10); - } - if (number >= -128 && number <= 127) { - return true; // byte values are compatible with all other numbers - } - if (number >= -32768 && number <= 32767) { - return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte - } - return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character - } - - /** - * @param {Token} token - */ - static from(token) { - function suffix(which) { - switch(which.indexOf(token.value.slice(-1))) { - case 0: - case 1: - return PrimitiveType.map.F; - case 2: - case 3: - return PrimitiveType.map.D; - case 4: - case 5: - return PrimitiveType.map.J; - } - } - switch(token.kind) { - case 'dec-exp-number-literal': - case 'dec-number-literal': - return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.D); - case 'hex-number-literal': - return new LiteralNumber(token.value, token.kind, suffix(' Ll') || PrimitiveType.map.I); - case 'int-number-literal': - default: - return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.I); - } - } -} - -class MethodCall extends Value { - /** - * @param {string} name - * @param {ResolvedIdent} instance - * @param {Method} method - */ - constructor(name, instance, method) { - super(name, method.returnType); - this.instance = instance; - this.method = method; - } -} - -class ConstructorCall extends Value { - /** - * @param {string} name - * @param {JavaType} type - */ - constructor(name, type) { - super(name, type); - } -} - -class ArrayLiteral extends LiteralValue { - /** - * @param {string} name - * @param {ResolvedIdent[]} elements - */ - constructor(name, elements) { - super(name, null); - this.elements = elements; - } -} - -class TernaryValue extends Value { - /** - * @param {string} name - * @param {JavaType} true_type - * @param {Token} colon - * @param {Value} false_value - */ - constructor(name, true_type, colon, false_value) { - super(name, true_type); - this.colon = colon; - this.falseValue = false_value; - } -} - -class ResolvedIdent { - - /** - * @param {string} ident - * @param {(Local|Parameter|Field|ArrayElement|Value)[]} variables - * @param {Method[]} methods - * @param {JavaType[]} types - * @param {string} package_name - */ - constructor(ident, variables = [], methods = [], types = [], package_name = '') { - this.source = ident; - this.variables = variables; - this.methods = methods; - this.types = types; - this.package_name = package_name; - } - -} +exports.addproblem = addproblem; exports.parseBody = parseBody; diff --git a/langserver/java/body-types.js b/langserver/java/body-types.js new file mode 100644 index 0000000..f78e286 --- /dev/null +++ b/langserver/java/body-types.js @@ -0,0 +1,356 @@ +const { JavaType, ArrayType, PrimitiveType, Method, Parameter, Field } = require('java-mti'); +const { Token } = require('./tokenizer'); + +class ResolvedIdent { + /** + * @param {string} ident + * @param {(Local|Parameter|Field|ArrayElement|Value)[]} variables + * @param {Method[]} methods + * @param {JavaType[]} types + * @param {string} package_name + */ + constructor(ident, variables = [], methods = [], types = [], package_name = '') { + this.source = ident; + this.variables = variables; + this.methods = methods; + this.types = types; + this.package_name = package_name; + } +} + +/** + * AnyType is a special type that's used to fill in types that are missing. + * To prevent cascading errors, AnyType should be fully assign/cast/type-compatible + * with any other type + */ +class AnyType extends JavaType { + /** + * + * @param {String} label + */ + constructor(label) { + super("class", [], ''); + super.simpleTypeName = label; + } + + static Instance = new AnyType(''); + + get rawTypeSignature() { + return 'U'; + } + + get typeSignature() { + return 'U'; + } +} + +class AnyMethod extends Method { + /** + * @param {string} name + */ + constructor(name) { + super(name, [], ''); + } + + get returnType() { + return AnyType.Instance; + } +} + +class Local { + /** + * @param {Token[]} modifiers + * @param {string} name + * @param {Token} decltoken + * @param {JavaType} type + * @param {JavaType} type + * @param {number} postnamearrdims + */ + constructor(modifiers, name, decltoken, type, postnamearrdims) { + this.finalToken = modifiers.find(m => m.source === 'final') || null; + this.name = name; + this.decltoken = decltoken; + if (postnamearrdims > 0) { + this.type = (type instanceof ArrayType) + ? new ArrayType(type.base, type.arrdims + postnamearrdims) + : new ArrayType(type, postnamearrdims); + } else { + this.type = type; + } + this.init = null; + } +} + +class ArrayElement { + /** + * + * @param {Local|Parameter|Field|ArrayElement|Value} array_variable + * @param {ResolvedIdent} index + */ + constructor(array_variable, index) { + this.array_variable = array_variable; + this.index = index; + if (!(this.array_variable.type instanceof ArrayType)) { + throw new Error('Array element cannot be created from non-array type'); + } + this.name = `${array_variable.name}[${index.source}]`; + /** @type {JavaType} */ + this.type = this.array_variable.type.elementType; + } +} + +class Value { + /** + * @param {string} name + * @param {JavaType} type + */ + constructor(name, type) { + this.name = name; + this.type = type; + } + + /** + * @param {string} ident + * @param {ResolvedIdent} lhs + * @param {ResolvedIdent} rhs + * @param {JavaType} type + */ + static build(ident, lhs, rhs, type) { + if (!lhs.variables[0] || !rhs.variables[0]) { + return new Value(ident, type); + } + if (lhs.variables[0] instanceof LiteralValue && rhs.variables && rhs.variables[0] instanceof LiteralValue) { + new LiteralValue(ident, type); + } + return new Value(ident, type); + } +} + +class AnyValue extends Value { + constructor(name) { + super(name, AnyType.Instance); + } +} + +class LiteralValue extends Value { } + +/** + * LiteralNumberType is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc). + * + * It's used to allow literal numbers to be type-assignable to variables with different primitive types. + * For example, 200 is type-assignable to short, int, long, float and double, but not byte. + */ +class LiteralNumber extends LiteralValue { + /** + * @param {string} value + * @param {string} kind + * @param {PrimitiveType} default_type + */ + constructor(value, kind, default_type) { + super(value, default_type); + this.numberValue = value; + this.numberKind = kind; + } + + static shift(a, b, op) { + const ai = a.toInt(), bi = b.toInt(); + if (ai === null || bi === null) { + return null; + } + const val = op(ai, bi); + const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I; + return new LiteralNumber(val.toString(), 'int-number-literal', type); + } + + static bitwise(a, b, op) { + const ai = a.toInt(), bi = b.toInt(); + if (ai === null || bi === null) { + return null; + } + const val = op(ai, bi); + const typekey = a.type.typeSignature+ b.type.typeSignature; + let type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I; + return new LiteralNumber(val.toString(), 'int-number-literal', type); + } + + static math(a, b, op, divmod) { + const ai = a.toNumber(), bi = b.toNumber(); + if (bi === 0 && divmod) { + return null; + } + let val = op(ai, bi); + const typekey = a.type.typeSignature+ b.type.typeSignature; + if (!/[FD]/.test(typekey) && divmod) { + val = Math.trunc(val); + } + let type; + if (/^(D|F[^D]|J[^FD])/.test(typekey)) { + type = a.type; + } else { + type = b.type; + } + return new LiteralNumber(val.toString(), 'int-number-literal', type); + } + + static '+'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a + b) } + static '-'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a - b) } + static '*'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a * b) } + static '/'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a / b, true) } + static '%'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a % b, true) } + static '&'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a & b) } + static '|'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a | b) } + static '^'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a ^ b) } + static '>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a >> b) } + static '>>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => { + // unsigned shift (>>>) is not supported by bigints + // @ts-ignore + return (a >> b) & ~(-1n << (64n - b)); + }) } + static '<<'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a << b) } + + toInt() { + switch (this.numberKind) { + case 'hex-number-literal': + case 'int-number-literal': + // unlike parseInt, BigInt doesn't like invalid characters, so + // ensure we strip any trailing long specifier + return BigInt(this.name.match(/(.+?)[lL]?$/)[1]); + } + return null; + } + + toNumber() { + return parseFloat(this.name); + } + + /** + * @param {JavaType} type + */ + isCompatibleWith(type) { + if (this.type === type) { + return true; + } + switch(this.type.simpleTypeName) { + case 'double': + return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature); + case 'float': + return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature); + } + // all integral types are all compatible with long, float and double variables + if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) { + return true; + } + // the desintation type must be a number primitive or one of the corresponding boxed classes + if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) { + return false; + } + let number = 0; + if (this.numberKind === 'hex-number-literal') { + if (this.numberValue !== '0x') { + const non_leading_zero_digits = this.numberValue.match(/0x0*(.+)/)[1]; + number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16); + } + } else if (this.numberKind === 'int-number-literal') { + const non_leading_zero_digits = this.numberValue.match(/0*(.+)/)[1]; + number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10); + } + if (number >= -128 && number <= 127) { + return true; // byte values are compatible with all other numbers + } + if (number >= -32768 && number <= 32767) { + return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte + } + return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character + } + + /** + * @param {Token} token + */ + static from(token) { + function suffix(which) { + switch(which.indexOf(token.value.slice(-1))) { + case 0: + case 1: + return PrimitiveType.map.F; + case 2: + case 3: + return PrimitiveType.map.D; + case 4: + case 5: + return PrimitiveType.map.J; + } + } + switch(token.kind) { + case 'dec-exp-number-literal': + case 'dec-number-literal': + return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.D); + case 'hex-number-literal': + return new LiteralNumber(token.value, token.kind, suffix(' Ll') || PrimitiveType.map.I); + case 'int-number-literal': + default: + return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.I); + } + } +} + +class MethodCall extends Value { + /** + * @param {string} name + * @param {ResolvedIdent} instance + * @param {Method} method + */ + constructor(name, instance, method) { + super(name, method.returnType); + this.instance = instance; + this.method = method; + } +} + +class ConstructorCall extends Value { + /** + * @param {string} name + * @param {JavaType} type + */ + constructor(name, type) { + super(name, type); + } +} + +class ArrayLiteral extends LiteralValue { + /** + * @param {string} name + * @param {ResolvedIdent[]} elements + */ + constructor(name, elements) { + super(name, null); + this.elements = elements; + } +} + +class TernaryValue extends Value { + /** + * @param {string} name + * @param {JavaType} true_type + * @param {Token} colon + * @param {Value} false_value + */ + constructor(name, true_type, colon, false_value) { + super(name, true_type); + this.colon = colon; + this.falseValue = false_value; + } +} + +exports.ResolvedIdent = ResolvedIdent; +exports.AnyType = AnyType; +exports.AnyMethod = AnyMethod; +exports.Local = Local; +exports.ArrayElement = ArrayElement; +exports.Value = Value; +exports.AnyValue = AnyValue; +exports.LiteralValue = LiteralValue; +exports.LiteralNumber = LiteralNumber; +exports.MethodCall = MethodCall; +exports.ConstructorCall = ConstructorCall; +exports.ArrayLiteral = ArrayLiteral; +exports.TernaryValue = TernaryValue; diff --git a/langserver/java/type-resolver.js b/langserver/java/type-resolver.js index 923d7b3..d6ca474 100644 --- a/langserver/java/type-resolver.js +++ b/langserver/java/type-resolver.js @@ -257,10 +257,109 @@ function resolveTypeIdents(types, fully_qualified_scope, resolved_imports, typem } +/** + * + * @param {string} ident + * @param {CEIType} scoped_type + * @param {ResolvedImport[]} imports + * @param {Map} typemap + */ +function resolveTypeOrPackage(ident, scoped_type, imports, typemap) { + const types = []; + let package_name = ''; + + // is it an enclosed type of the currently scoped type or any outer type + if (scoped_type) { + const scopes = scoped_type.shortSignature.split('$'); + while (scopes.length) { + const enc_type = typemap.get(`${scopes.join('$')}$${ident}`); + if (enc_type) { + types.push(enc_type); + break; + } + scopes.pop(); + } + } + + if (!types[0]) { + // is it a top-level type from the imports + const top_level_type = '/' + ident; + for (let i of imports) { + const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(top_level_type)); + if (fqn) { + types.push(i.types.get(fqn)); + } + } + } + + if (!types[0]) { + // is it a default-package type + const default_type = typemap.get(ident); + if (default_type) { + types.push(default_type); + } + } + + // the final option is the start of a package name + const package_root = ident + '/'; + const typelist = [...typemap.keys()]; + if (typelist.find(fqn => fqn.startsWith(package_root))) { + package_name = ident; + } + + return { + types, + package_name, + } +} + +/** + * + * @param {string} ident + * @param {JavaType[]} outer_types + * @param {string} outer_package_name + * @param {Map} 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_match = `${outer_package_name}/${ident}`; + if (typemap.has(type_match)) { + // it matches a type + types.push(typemap.get(type_match)); + } + const package_match = type_match + '/'; + if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) { + // it matches a sub-package + package_name = type_match; + } + } + + return { + types, + package_name, + } +} + module.exports = { parse_type, resolveType, resolveTypes, resolveTypeIdents, ResolvedType, + resolveTypeOrPackage, + resolveNextTypeOrPackage, } diff --git a/langserver/java/typeident.js b/langserver/java/typeident.js new file mode 100644 index 0000000..e4d6520 --- /dev/null +++ b/langserver/java/typeident.js @@ -0,0 +1,94 @@ +const { JavaType, WildcardType } = require('java-mti'); +const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-type'); +const ResolvedImport = require('./parsetypes/resolved-import'); +const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver'); +const { Token } = require('./tokenizer'); +const { AnyType, ResolvedIdent } = require("./body-types"); + +/** + * @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMC + * @typedef {import('./TokenList').TokenList} TokenList + */ + + /** + * @param {TokenList} tokens + * @param {SourceMC} method + * @param {ResolvedImport[]} imports + * @param {Map} typemap + */ +function typeIdentList(tokens, method, imports, typemap) { + let type = typeIdent(tokens, method, imports, typemap); + const types = [type]; + while (tokens.current.value === ',') { + tokens.inc(); + type = typeIdent(tokens, method, imports, typemap); + types.push(type); + } + return types; +} + +/** + * @param {TokenList} tokens + * @param {SourceMC} method + * @param {ResolvedImport[]} imports + * @param {Map} typemap + */ +function typeIdent(tokens, method, imports, typemap) { + if (tokens.current.kind !== 'ident') { + if (tokens.current.value === '?') { + return wildcardTypeArgument(tokens, method, imports, typemap); + } + return AnyType.Instance; + } + const { types, package_name } = resolveTypeOrPackage(tokens.current.value, method._owner, imports, typemap); + tokens.inc(); + for (;;) { + if (tokens.isValue('.')) { + if (tokens.current.kind !== 'ident') { + break; + } + resolveNextTypeOrPackage(tokens.current.value, types, package_name, typemap); + } else if (tokens.isValue('<')) { + if (!tokens.isValue('>')) { + typeIdentList(tokens, method, imports, typemap); + if (/>>>?/.test(tokens.current.value)) { + // we need to split >> and >>> into separate > tokens to handle things like List> + 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('>'); + } + } else { + break; + } + } + + return types[0] || AnyType.Instance; +} + +/** + * @param {TokenList} tokens + * @param {SourceMC} method + * @param {ResolvedImport[]} imports + * @param {Map} typemap + * @returns {WildcardType} + */ +function wildcardTypeArgument(tokens, method, 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: typeIdent(tokens, method, imports, typemap), + } + break; + } + return new WildcardType(bound); +} + +exports.typeIdent = typeIdent; +exports.typeIdentList = typeIdentList;