mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +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:
0
langserver/.library-cache/android-29.zip
Normal file
0
langserver/.library-cache/android-29.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
143
langserver/analytics.js
Normal file
143
langserver/analytics.js
Normal file
@@ -0,0 +1,143 @@
|
||||
let mp;
|
||||
/** @type {string} */
|
||||
let uid;
|
||||
/** @type {string} */
|
||||
let sid;
|
||||
/** @type {Map<string,[number,number]>} */
|
||||
const timeLabels = new Map();
|
||||
let session_start = Date.now();
|
||||
|
||||
/**
|
||||
* @param {string} [t]
|
||||
* @param {string} [u]
|
||||
* @param {string} [s]
|
||||
* @param {{name:string,version:string}} [package_json]
|
||||
* @param {*} [props]
|
||||
*/
|
||||
function init(t = '0cca95950055c6553804a46ce7e3df18', u, s, package_json, props) {
|
||||
if (mp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mp = require('mixpanel').init(t);
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
uid = u;
|
||||
sid = s;
|
||||
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
const os = require('os');
|
||||
const now = new Date();
|
||||
event(`${package_json.name}-start`, {
|
||||
extension: package_json.name,
|
||||
ext_version: package_json.version,
|
||||
arch: process.arch,
|
||||
cpus: os.cpus().length,
|
||||
mem: (os.totalmem() / 1e6)|0,
|
||||
platform: process.platform,
|
||||
node_version: process.version,
|
||||
release: os.release(),
|
||||
localtime: now.toTimeString(),
|
||||
tz: now.getTimezoneOffset(),
|
||||
...props
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @param {*} [properties]
|
||||
*/
|
||||
function event(eventName, properties) {
|
||||
if (!mp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (uid) {
|
||||
mp.track(eventName, {
|
||||
distinct_id: uid,
|
||||
session_id: sid,
|
||||
session_length: Math.trunc((Date.now() - session_start) / 60e3),
|
||||
...properties,
|
||||
});
|
||||
} else {
|
||||
mp.track(eventName, properties);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
*/
|
||||
function time(label) {
|
||||
if (!label || timeLabels.has(label)) {
|
||||
return;
|
||||
}
|
||||
timeLabels.set(label, process.hrtime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @param {'ns'|'us'|'ms'|'s'} time_unit
|
||||
* @param {*} [additionalProps]
|
||||
*/
|
||||
function timeEnd(label, time_unit = 'ms', additionalProps = {}) {
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
const startTime = timeLabels.get(label);
|
||||
timeLabels.delete(label);
|
||||
if (!Array.isArray(startTime)) {
|
||||
return;
|
||||
}
|
||||
const elapsed = process.hrtime(startTime);
|
||||
const count = time_unit === 's' ? elapsed[0] : ((elapsed[0]*1e9) + elapsed[1]);
|
||||
const divs = {
|
||||
ns: 1, us: 1e3, ms: 1e6, s: 1
|
||||
}
|
||||
const props = {
|
||||
[`${label}-elapsed`]: Math.trunc(count / (divs[time_unit] || 1)),
|
||||
[`${label}-elapsed_unit`]: time_unit,
|
||||
...additionalProps,
|
||||
}
|
||||
event(label, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode').ExtensionContext} context
|
||||
*/
|
||||
function getIDs(context) {
|
||||
if (!context || !context.globalState) {
|
||||
return {
|
||||
uid: '', sid: ''
|
||||
};
|
||||
}
|
||||
let uuidv4 = () => {
|
||||
try {
|
||||
uuidv4 = require('uuid').v4;
|
||||
return uuidv4();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
let u = uid || (uid = context.globalState.get('mix-panel-id'));
|
||||
if (typeof u !== 'string' || u.length > 36) {
|
||||
u = uid = uuidv4();
|
||||
context.globalState.update('mix-panel-id', u);
|
||||
}
|
||||
let s = sid || (sid = uuidv4());
|
||||
return {
|
||||
uid: u,
|
||||
sid: s,
|
||||
}
|
||||
}
|
||||
|
||||
exports.init = init;
|
||||
exports.event = event;
|
||||
exports.time = time;
|
||||
exports.timeEnd = timeEnd;
|
||||
exports.getIDs = getIDs;
|
||||
113
langserver/build.js
Normal file
113
langserver/build.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* This is a really basic module packer. It simply loads all the modules into a
|
||||
* single object, appends a simple bootstrap require function to 'load' the modules at
|
||||
* runtime and then writes the result out to a entry module.
|
||||
*
|
||||
* - Each local module (i.e not in node_modules) must be a relative path starting with . or ..
|
||||
* - Subfolders are allowed, but only js files are included.
|
||||
*/
|
||||
|
||||
/** These are the sources (files and folders) we want to pack */
|
||||
const sources = [
|
||||
'analytics.js',
|
||||
'completions.js',
|
||||
'doc-formatter.js',
|
||||
'document.js',
|
||||
'java',
|
||||
'logging.js',
|
||||
'method-signatures.js',
|
||||
'server.js',
|
||||
'settings.js'
|
||||
]
|
||||
|
||||
/** The entry module - must have a relative path */
|
||||
const entry = './server.js';
|
||||
|
||||
const fs = require('fs');
|
||||
const modules = [];
|
||||
|
||||
while (sources.length) {
|
||||
const source = sources.shift();
|
||||
const stat = fs.statSync(source);
|
||||
if (stat.isDirectory()) {
|
||||
fs.readdirSync(source).forEach(entry => {
|
||||
sources.unshift(`${source}/${entry}`);
|
||||
})
|
||||
continue;
|
||||
}
|
||||
if (!source.endsWith('.js')) {
|
||||
console.log(`ignoring non-js file: ${source}`);
|
||||
continue;
|
||||
}
|
||||
// add an object entry of the form: 'path': (...) => { file_content }
|
||||
modules.push(`'${source}':
|
||||
(require,module,exports) => {
|
||||
${fs.readFileSync(source, 'utf8')}
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The bootstrap contains the custom require function and the call to load
|
||||
* the initial module - it's everything after the marker below
|
||||
*/
|
||||
const bootstrap = fs.readFileSync(__filename, 'utf8').split('/* bootstrap marker */').pop();
|
||||
|
||||
fs.writeFileSync(entry,
|
||||
`const data = {
|
||||
${modules.join(',\n')}
|
||||
}
|
||||
${bootstrap}
|
||||
_require('${entry}');
|
||||
`);
|
||||
|
||||
/* bootstrap marker */
|
||||
|
||||
const module_stack = [{
|
||||
path: [],
|
||||
name: '',
|
||||
}]
|
||||
|
||||
const loadedModules = new Set();
|
||||
|
||||
function _require(filename) {
|
||||
// local modules always have a relative path
|
||||
if (!filename.startsWith('.')) {
|
||||
// node_modules import
|
||||
return require(filename);
|
||||
}
|
||||
const new_path = module_stack[0].path.slice();
|
||||
let key = filename.replace(/(\.js)?$/, '.js');
|
||||
for (let m; m = key.match(/^\.\.?\//);) {
|
||||
key = key.slice(m[0].length);
|
||||
if (m[0] === '../') {
|
||||
new_path.pop();
|
||||
}
|
||||
}
|
||||
key = [...new_path, key].join('/');
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
throw new Error(`Missing module: ${key}`);
|
||||
}
|
||||
|
||||
const entry = data[key];
|
||||
|
||||
if (loadedModules.has(key)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
const path_parts = key.split(/[\\/]/);
|
||||
|
||||
module_stack.unshift({
|
||||
name: path_parts.pop(),
|
||||
path: path_parts,
|
||||
})
|
||||
const mod = {
|
||||
exports: {},
|
||||
}
|
||||
entry(_require, mod, mod.exports);
|
||||
module_stack.shift();
|
||||
|
||||
loadedModules.add(key);
|
||||
return data[key] = mod.exports;
|
||||
}
|
||||
514
langserver/completions.js
Normal file
514
langserver/completions.js
Normal file
@@ -0,0 +1,514 @@
|
||||
const { JavaType, CEIType, ArrayType, PrimitiveType } = require('java-mti');
|
||||
const { getTypeInheritanceList } = require('./java/expression-resolver');
|
||||
const { CompletionItem, CompletionItemKind } = require('vscode-languageserver');
|
||||
const { SourceType } = require('./java/source-types');
|
||||
const { indexAt } = require('./document');
|
||||
const { formatDoc } = require('./doc-formatter');
|
||||
const { trace } = require('./logging');
|
||||
const { event } = require('./analytics');
|
||||
|
||||
/**
|
||||
* Case-insensitive sort routines
|
||||
*/
|
||||
const sortBy = {
|
||||
label: (a,b) => a.label.localeCompare(b.label, undefined, {sensitivity: 'base'}),
|
||||
name: (a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}),
|
||||
}
|
||||
|
||||
/** Map Java typeKind values to vscode CompletionItemKinds */
|
||||
const TypeKindMap = {
|
||||
class: CompletionItemKind.Class,
|
||||
interface: CompletionItemKind.Interface,
|
||||
'@interface': CompletionItemKind.Interface,
|
||||
enum: CompletionItemKind.Enum,
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a list of vscode-compatible completion items for a given type.
|
||||
*
|
||||
* The type is located in typemap and the members (fields, methods) are retrieved
|
||||
* and converted to completions items.
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap Set of known types
|
||||
* @param {string} type_signature Type to provide completion items for
|
||||
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
|
||||
* @param {string[]} [typelist] optional pre-prepared type list (to save recomputing it)
|
||||
*/
|
||||
function getTypedNameCompletion(typemap, type_signature, opts, typelist) {
|
||||
let type, types, subtype_search;
|
||||
const arr_match = type_signature.match(/^\[+/);
|
||||
if (arr_match) {
|
||||
// for arrays, just create a dummy type
|
||||
types = [
|
||||
type = new ArrayType(PrimitiveType.map.V, arr_match[0].length),
|
||||
typemap.get('java/lang/Object'),
|
||||
];
|
||||
} else if (!/^L.+;/.test(type_signature)) {
|
||||
return [];
|
||||
} else {
|
||||
type = typemap.get(type_signature.slice(1,-1));
|
||||
if (!type) {
|
||||
return [];
|
||||
}
|
||||
if (!(type instanceof CEIType)) {
|
||||
return [];
|
||||
}
|
||||
// retrieve the complete list of inherited types
|
||||
types = getTypeInheritanceList(type);
|
||||
subtype_search = type.shortSignature + '$';
|
||||
}
|
||||
|
||||
|
||||
class SetOnceMap extends Map {
|
||||
set(key, value) {
|
||||
return this.has(key) ? this : super.set(key, value);
|
||||
}
|
||||
}
|
||||
const fields = new SetOnceMap(),
|
||||
methods = new SetOnceMap(),
|
||||
inner_types = new SetOnceMap(),
|
||||
enumValues = new SetOnceMap();
|
||||
|
||||
/**
|
||||
* @param {string[]} modifiers
|
||||
* @param {JavaType} t
|
||||
* @param {boolean} [synthetic]
|
||||
*/
|
||||
function shouldInclude(modifiers, t, synthetic) {
|
||||
// filter statics/instances
|
||||
if (opts.statics !== modifiers.includes('static')) return;
|
||||
// exclude synthetic entries
|
||||
if (synthetic) return;
|
||||
if (modifiers.includes('public')) return true;
|
||||
if (modifiers.includes('protected')) return true;
|
||||
// only include private items for the current type
|
||||
if (modifiers.includes('private') && t === type) return true;
|
||||
// @ts-ignore
|
||||
return t.packageName === type.packageName;
|
||||
}
|
||||
|
||||
// retrieve fields and methods
|
||||
types.forEach(t => {
|
||||
if (t instanceof SourceType && opts.statics) {
|
||||
t.enumValues.sort(sortBy.name)
|
||||
.forEach(e => enumValues.set(e.name, {e, t}))
|
||||
}
|
||||
t.fields.sort(sortBy.name)
|
||||
.filter(f => shouldInclude(f.modifiers, t, f.isSynthetic))
|
||||
.forEach(f => {
|
||||
if (f.isEnumValue) {
|
||||
enumValues.set(f.name, {e:f, t});
|
||||
} else {
|
||||
fields.set(f.name, {f, t});
|
||||
}
|
||||
});
|
||||
t.methods.sort(sortBy.name)
|
||||
.filter(m => shouldInclude(m.modifiers, t, m.isSynthetic))
|
||||
.forEach(m => methods.set(`${m.name}${m.methodSignature}`, {m, t}));
|
||||
});
|
||||
|
||||
if (opts.statics && subtype_search) {
|
||||
// retrieve inner types
|
||||
(typelist || [...typemap.keys()])
|
||||
.filter(type_signature =>
|
||||
type_signature.startsWith(subtype_search)
|
||||
// ignore inner-inner types
|
||||
&& !type_signature.slice(subtype_search.length).includes('$')
|
||||
)
|
||||
.map(type_signature => typemap.get(type_signature))
|
||||
.forEach((t,idx) => inner_types.set(t.simpleTypeName, { t }));
|
||||
}
|
||||
|
||||
return [
|
||||
// enum values
|
||||
...[...enumValues.values()].map((e,idx) => ({
|
||||
label: `${e.e.name}: ${e.t.simpleTypeName}`,
|
||||
insertText: e.e.name,
|
||||
kind: CompletionItemKind.EnumMember,
|
||||
sortText: `${idx+1000}${e.e.name}`,
|
||||
data: { type: e.t.shortSignature, fidx: e.t.fields.indexOf(e.e) },
|
||||
})),
|
||||
// fields
|
||||
...[...fields.values()].map((f,idx) => ({
|
||||
label: `${f.f.name}: ${f.f.type.simpleTypeName}`,
|
||||
insertText: f.f.name,
|
||||
kind: CompletionItemKind.Field,
|
||||
sortText: `${idx+2000}${f.f.name}`,
|
||||
data: { type: f.t.shortSignature, fidx: f.t.fields.indexOf(f.f) },
|
||||
})),
|
||||
// methods
|
||||
...[...methods.values()].map((m,idx) => ({
|
||||
label: m.m.shortlabel,
|
||||
kind: CompletionItemKind.Method,
|
||||
insertText: m.m.name,
|
||||
sortText: `${idx+3000}${m.m.name}`,
|
||||
data: { type: m.t.shortSignature, midx: m.t.methods.indexOf(m.m) },
|
||||
})),
|
||||
// types
|
||||
...[...inner_types.values()].map((it,idx) => ({
|
||||
label: it.t.simpleTypeName,
|
||||
kind: TypeKindMap[it.t.typeKind],
|
||||
sortText: `${idx+4000}${it.t.simpleTypeName}`,
|
||||
data: { type: it.shortSignature },
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of vscode-compatible completion items for a dotted identifier (package or type).
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap Set of known types
|
||||
* @param {string} dotted_name
|
||||
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
|
||||
*/
|
||||
function getFullyQualifiedDottedIdentCompletion(typemap, dotted_name, opts) {
|
||||
if (dotted_name === '') {
|
||||
// return the list of top-level package names
|
||||
return getTopLevelPackageCompletions(typemap);
|
||||
}
|
||||
// name is a fully dotted name, possibly including members and their fields
|
||||
let typelist = [...typemap.keys()];
|
||||
|
||||
const split_name = dotted_name.split('.');
|
||||
let pkgname = '';
|
||||
/** @type {JavaType} */
|
||||
let type = null, typename = '';
|
||||
for (let name_part of split_name) {
|
||||
if (type) {
|
||||
if (opts.statics && typelist.includes(`${typename}$${name_part}`)) {
|
||||
type = typemap.get(typename = `${typename}$${name_part}`);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
typename = pkgname + name_part;
|
||||
if (typelist.includes(typename)) {
|
||||
type = typemap.get(typename);
|
||||
continue;
|
||||
}
|
||||
pkgname = `${pkgname}${name_part}/`;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
return getTypedNameCompletion(typemap, type.typeSignature, opts, typelist);
|
||||
}
|
||||
|
||||
// sub-package or type
|
||||
const search_pkg = pkgname;
|
||||
return typelist.reduce((arr,typename) => {
|
||||
if (typename.startsWith(search_pkg)) {
|
||||
const m = typename.slice(search_pkg.length).match(/^(.+?)(\/|$)/);
|
||||
if (m) {
|
||||
if (m[2]) {
|
||||
// package name
|
||||
if (!arr.find(x => x.label === m[1])) {
|
||||
arr.push({
|
||||
label: m[1],
|
||||
kind: CompletionItemKind.Unit,
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// type name
|
||||
arr.push({
|
||||
label: m[1].replace(/\$/g,'.'),
|
||||
kind: CompletionItemKind.Class,
|
||||
data: { type: typename },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of completion items for top-level package names (e.g java, javax, android)
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function getTopLevelPackageCompletions(typemap) {
|
||||
const pkgs = [...typemap.keys()].reduce((set, short_type_signature) => {
|
||||
// the root package is the first part of the short type signature (up to the first /)
|
||||
const m = short_type_signature.match(/(.+?)\//);
|
||||
m && set.add(m[1]);
|
||||
return set;
|
||||
}, new Set());
|
||||
|
||||
const items = [...pkgs].filter(x => x)
|
||||
.sort()
|
||||
.map(package_ident => ({
|
||||
label: package_ident,
|
||||
kind: CompletionItemKind.Unit,
|
||||
sortText: package_ident,
|
||||
}));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {string} pkg
|
||||
*/
|
||||
function getPackageCompletion(typemap, pkg) {
|
||||
if (pkg === '') {
|
||||
return getTopLevelPackageCompletions(typemap);
|
||||
}
|
||||
// sub-package
|
||||
const search_pkg = pkg + '/';
|
||||
const pkgs = [...typemap.keys()].reduce((arr,typename) => {
|
||||
if (typename.startsWith(search_pkg)) {
|
||||
const m = typename.slice(search_pkg.length).match(/^(.+?)\//);
|
||||
if (m) arr.add(m[1]);
|
||||
}
|
||||
return arr;
|
||||
}, new Set());
|
||||
|
||||
return [...pkgs].filter(x => x).sort().map(pkg => ({
|
||||
label: pkg,
|
||||
kind: CompletionItemKind.Unit,
|
||||
data: null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Cache of completion items for fixed values, keywords and Android library types */
|
||||
let defaultCompletionTypes = null;
|
||||
|
||||
/** @type {Map<string,CEIType>} */
|
||||
let lastCompletionTypeMap = null;
|
||||
|
||||
let completionRequestCount = 0;
|
||||
|
||||
function initDefaultCompletionTypes(lib) {
|
||||
defaultCompletionTypes = {
|
||||
instances: 'this super'.split(' ').map(t => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Value,
|
||||
sortText: t
|
||||
})),
|
||||
// primitive types
|
||||
primitiveTypes:'boolean byte char double float int long short void'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Keyword,
|
||||
sortText: t,
|
||||
})),
|
||||
// modifiers
|
||||
modifiers: 'public private protected static final abstract volatile native transient strictfp synchronized'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Keyword,
|
||||
sortText: t,
|
||||
})),
|
||||
// literals
|
||||
literals: 'false true null'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Value,
|
||||
sortText: t
|
||||
})),
|
||||
// type names
|
||||
types: [...lib.values()].map(
|
||||
t =>
|
||||
/** @type {CompletionItem} */
|
||||
({
|
||||
label: t.dottedTypeName,
|
||||
kind: TypeKindMap[t.typeKind],
|
||||
data: { type: t.shortSignature },
|
||||
sortText: t.dottedTypeName,
|
||||
})
|
||||
).sort(sortBy.label),
|
||||
// package names
|
||||
packageNames: getTopLevelPackageCompletions(lib),
|
||||
}
|
||||
}
|
||||
|
||||
function clearDefaultCompletionEntries() {
|
||||
defaultCompletionTypes = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the VSCode completion item request.
|
||||
*
|
||||
* @param {import('vscode-languageserver').CompletionParams} params
|
||||
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
*/
|
||||
async function getCompletionItems(params, liveParsers, androidLibrary) {
|
||||
trace('getCompletionItems');
|
||||
|
||||
if (!params || !params.textDocument || !params.textDocument.uri) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let dct = defaultCompletionTypes;
|
||||
if (!defaultCompletionTypes) {
|
||||
initDefaultCompletionTypes(androidLibrary);
|
||||
dct = defaultCompletionTypes || {};
|
||||
}
|
||||
|
||||
// wait for the Android library to load (in case we receive an early request)
|
||||
if (androidLibrary instanceof Promise) {
|
||||
androidLibrary = await androidLibrary;
|
||||
}
|
||||
|
||||
// retrieve the parsed source corresponding to the request URI
|
||||
const docinfo = liveParsers.get(params.textDocument.uri);
|
||||
if (!docinfo || !docinfo.parsed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// wait for the user to stop typing
|
||||
const preversion = docinfo.version;
|
||||
await docinfo.reparseWaiter;
|
||||
if (docinfo.version !== preversion) {
|
||||
// if the file content has changed since this request wss made, ignore it
|
||||
trace('content changed - ignoring completion items')
|
||||
/** @type {import('vscode-languageserver').CompletionList} */
|
||||
return {
|
||||
isIncomplete: true,
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
|
||||
completionRequestCount += 1;
|
||||
if ((completionRequestCount === 1) || (completionRequestCount === 5) || ((completionRequestCount % 25) === 0)) {
|
||||
event('completion-requests', { comp_req_count: completionRequestCount });
|
||||
}
|
||||
|
||||
let parsed = docinfo.parsed;
|
||||
|
||||
// save the typemap associated with this parsed state - we use this when resolving
|
||||
// the documentation later
|
||||
lastCompletionTypeMap = (parsed && parsed.typemap) || androidLibrary;
|
||||
|
||||
let locals = [],
|
||||
modifiers = dct.modifiers,
|
||||
type_members = [],
|
||||
sourceTypes = [];
|
||||
|
||||
if (parsed.unit) {
|
||||
const char_index = indexAt(params.position, parsed.content);
|
||||
const options = parsed.unit.getCompletionOptionsAt(char_index);
|
||||
|
||||
if (options.loc) {
|
||||
if (/^pkgname:/.test(options.loc.key)) {
|
||||
return getPackageCompletion(parsed.typemap, options.loc.key.split(':').pop());
|
||||
}
|
||||
if (/^fqdi:/.test(options.loc.key)) {
|
||||
// fully-qualified dotted identifier
|
||||
return getFullyQualifiedDottedIdentCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
|
||||
}
|
||||
if (/^fqs:/.test(options.loc.key)) {
|
||||
// fully-qualified static expression
|
||||
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
|
||||
}
|
||||
if (/^fqi:/.test(options.loc.key)) {
|
||||
// fully-qualified instance expression
|
||||
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: false });
|
||||
}
|
||||
}
|
||||
|
||||
// if this token is inside a method, include the parameters and this/super
|
||||
if (options.method) {
|
||||
locals = options.method.parameters
|
||||
.sort(sortBy.name)
|
||||
.map(p => ({
|
||||
label: `${p.name}: ${p.type.simpleTypeName}`,
|
||||
insertText: p.name,
|
||||
kind: CompletionItemKind.Variable,
|
||||
sortText: p.name,
|
||||
}));
|
||||
|
||||
// if this is not a static method, include this/super
|
||||
if (!options.method.modifiers.includes('static')) {
|
||||
locals.push(...dct.instances);
|
||||
}
|
||||
|
||||
type_members = getTypedNameCompletion(
|
||||
parsed.typemap,
|
||||
options.method.owner.typeSignature,
|
||||
{ statics: !!options.method.modifierTokens.find(m => m.value === 'static') }
|
||||
);
|
||||
|
||||
// if we're inside a method, don't show the modifiers
|
||||
modifiers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// add types currently parsed from the source files
|
||||
liveParsers.forEach(doc => {
|
||||
if (!doc.parsed) {
|
||||
return;
|
||||
}
|
||||
doc.parsed.unit.types.forEach(
|
||||
t => sourceTypes.push({
|
||||
label: t.dottedTypeName,
|
||||
kind: TypeKindMap[t.typeKind],
|
||||
data: { type:t.shortSignature },
|
||||
sortText: t.dottedTypeName,
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
// exclude dotted (inner) types because they result in useless
|
||||
// matches in the intellisense filter when . is pressed
|
||||
const types = [
|
||||
...dct.types,
|
||||
...sourceTypes,
|
||||
].filter(x => !x.label.includes('.'))
|
||||
.sort(sortBy.label)
|
||||
|
||||
return [
|
||||
...locals,
|
||||
...type_members,
|
||||
...dct.primitiveTypes,
|
||||
...dct.literals,
|
||||
...modifiers,
|
||||
...types,
|
||||
...dct.packageNames,
|
||||
].map((x,idx) => {
|
||||
// to force the order above, reset sortText for each item based upon a fixed-length number
|
||||
x.sortText = `${1000+idx}`;
|
||||
return x;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the detail and documentation for the specified item
|
||||
*
|
||||
* @param {CompletionItem} item
|
||||
*/
|
||||
function resolveCompletionItem(item) {
|
||||
item.detail = item.documentation = '';
|
||||
if (!lastCompletionTypeMap) {
|
||||
return item;
|
||||
}
|
||||
if (!item.data || typeof item.data !== 'object') {
|
||||
return item;
|
||||
}
|
||||
const type = lastCompletionTypeMap.get(item.data.type);
|
||||
const field = type && type.fields[item.data.fidx];
|
||||
const method = type && type.methods[item.data.midx];
|
||||
if (!type) {
|
||||
return item;
|
||||
}
|
||||
let detail, documentation, header;
|
||||
if (field) {
|
||||
detail = field.label;
|
||||
documentation = field.docs;
|
||||
header = `${field.type.simpleTypeName} **${field.name}**`;
|
||||
} else if (method) {
|
||||
detail = `${method.modifiers.filter(m => !/abstract|transient|native/.test(m)).join(' ')} ${type.simpleTypeName}.${method.name}`;
|
||||
documentation = method.docs;
|
||||
header = method.shortlabel.replace(/^\w+/, x => `**${x}**`).replace(/^(.+?)\s*:\s*(.+)/, (_,a,b) => `${b} ${a}`);
|
||||
} else {
|
||||
detail = type.fullyDottedRawName,
|
||||
documentation = type.docs,
|
||||
header = `${type.typeKind} **${type.dottedTypeName}**`;
|
||||
}
|
||||
item.detail = detail || '';
|
||||
item.documentation = formatDoc(header, documentation);
|
||||
return item;
|
||||
}
|
||||
|
||||
exports.getCompletionItems = getCompletionItems;
|
||||
exports.resolveCompletionItem = resolveCompletionItem;
|
||||
exports.clearDefaultCompletionEntries = clearDefaultCompletionEntries;
|
||||
35
langserver/doc-formatter.js
Normal file
35
langserver/doc-formatter.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Convert JavaDoc content to markdown used by vscode.
|
||||
*
|
||||
* This is a *very* rough conversion, simply looking for HTML tags and replacing them
|
||||
* with relevant markdown characters.
|
||||
* It is neither complete, nor perfect.
|
||||
*
|
||||
* @param {string} header
|
||||
* @param {string} documentation
|
||||
* @returns {import('vscode-languageserver').MarkupContent}
|
||||
*/
|
||||
function formatDoc(header, documentation) {
|
||||
return {
|
||||
kind: 'markdown',
|
||||
value: `${header ? header + '\n\n' : ''}${
|
||||
(documentation || '')
|
||||
.replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '')
|
||||
.replace(/(\n[ \t]*@[a-z]+)|(<p(?: .*)?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => {
|
||||
return prm ? ` ${prm}`
|
||||
: p ? '\n\n'
|
||||
: i ? '*'
|
||||
: b ? '**'
|
||||
: tt ? '`'
|
||||
: c ? '\n```'
|
||||
: lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``)
|
||||
: li ? '\n- '
|
||||
: a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`)
|
||||
: h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} `
|
||||
: '';
|
||||
})
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
exports.formatDoc = formatDoc;
|
||||
283
langserver/document.js
Normal file
283
langserver/document.js
Normal file
@@ -0,0 +1,283 @@
|
||||
const { CEIType } = require('java-mti');
|
||||
const ParseProblem = require('./java/parsetypes/parse-problem');
|
||||
const { parse } = require('./java/body-parser');
|
||||
const { SourceUnit } = require('./java/source-types');
|
||||
const { parseMethodBodies } = require('./java/validater');
|
||||
const { time, timeEnd, trace } = require('./logging');
|
||||
|
||||
/**
|
||||
* Marker to prevent early parsing of source files before we've completed our
|
||||
* initial source file load (we cannot accurately parse individual files until we
|
||||
* know what all the types are - hence the need to perform a first parse of all the source files).
|
||||
*
|
||||
* While we are waiting for the first parse to complete, individual files-to-parse are added
|
||||
* to this set. Once the first scan and parse is done, these are reparsed and
|
||||
* first_parse_waiting is set to `null`.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
let first_parse_waiting = new Set();
|
||||
|
||||
/**
|
||||
* Convert a line,character position to an absolute character offset
|
||||
*
|
||||
* @param {{line:number,character:number}} pos
|
||||
* @param {string} content
|
||||
*/
|
||||
function indexAt(pos, content) {
|
||||
let idx = 0;
|
||||
for (let i = 0; i < pos.line; i++) {
|
||||
idx = content.indexOf('\n', idx) + 1;
|
||||
if (idx === 0) {
|
||||
return content.length;
|
||||
}
|
||||
}
|
||||
return Math.min(idx + pos.character, content.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an absolute character offset to a line,character position
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {string} content
|
||||
*/
|
||||
function positionAt(index, content) {
|
||||
let line = 0,
|
||||
last_nl_idx = 0,
|
||||
character = 0;
|
||||
if (index <= 0) return { line, character };
|
||||
for (let idx = 0; ;) {
|
||||
idx = content.indexOf('\n', idx) + 1;
|
||||
if (idx === 0 || idx > index) {
|
||||
if (idx === 0) index = content.length;
|
||||
character = index - last_nl_idx;
|
||||
return { line, character };
|
||||
}
|
||||
last_nl_idx = idx;
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialised Map to allow for case-insensitive fileURIs on Windows.
|
||||
*
|
||||
* For cs-filesystems, this should work as a normal map.
|
||||
* For ci-filesystems, if a file URI case changes, it should be picked up
|
||||
* by the lowercase map
|
||||
*/
|
||||
class FileURIMap extends Map {
|
||||
lowerMap = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
get(key) {
|
||||
return super.get(key) || this.lowerMap.get(key.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
has(key) {
|
||||
return super.has(key) || this.lowerMap.has(key.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
this.lowerMap.set(key.toLowerCase(), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
delete(key) {
|
||||
this.lowerMap.delete(key.toLowerCase());
|
||||
return super.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
this.lowerMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for storing data about Java source files
|
||||
*/
|
||||
class JavaDocInfo {
|
||||
/**
|
||||
* @param {string} uri the file URI
|
||||
* @param {string} content the full file content
|
||||
* @param {number} version revision number for edited files (each edit increments the version)
|
||||
*/
|
||||
constructor(uri, content, version) {
|
||||
this.uri = uri;
|
||||
this.content = content;
|
||||
this.version = version;
|
||||
/**
|
||||
* The result of the Java parse
|
||||
* @type {ParsedInfo}
|
||||
*/
|
||||
this.parsed = null;
|
||||
|
||||
/**
|
||||
* Promise linked to a timer which resolves a short time after the user stops typing
|
||||
* - This is used to prevent constant reparsing while the user is typing in the document
|
||||
* @type {Promise}
|
||||
*/
|
||||
this.reparseWaiter = Promise.resolve();
|
||||
|
||||
/** @type {{ resolve: () => void, timer: * }} */
|
||||
this.waitInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this document for reparsing.
|
||||
*
|
||||
* To prevent redundant parsing while typing, a small delay is required
|
||||
* before the reparse happens.
|
||||
* When a key is pressed, `scheduleReparse()` starts a timer. If more
|
||||
* keys are typed before the timer expires, the timer is restarted.
|
||||
* Once typing pauses, the timer expires and the content reparsed.
|
||||
*
|
||||
* A `reparseWaiter` promise is used to delay actions like completion items
|
||||
* retrieval and method signature resolving until the reparse is complete.
|
||||
*
|
||||
* @param {Map<string,JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
*/
|
||||
scheduleReparse(liveParsers, androidLibrary) {
|
||||
const createWaitTimer = () => {
|
||||
return setTimeout(() => {
|
||||
// reparse the content, resolve the reparseWaiter promise
|
||||
// and reset the fields
|
||||
reparse([this.uri], liveParsers, androidLibrary, { includeMethods: true });
|
||||
this.waitInfo.resolve();
|
||||
this.waitInfo = null;
|
||||
}, 250);
|
||||
}
|
||||
if (this.waitInfo) {
|
||||
// we already have a promise pending - just restart the timer
|
||||
trace('restart timer');
|
||||
clearTimeout(this.waitInfo.timer);
|
||||
this.waitInfo.timer = createWaitTimer();
|
||||
return;
|
||||
}
|
||||
// create a new pending promise and start the timer
|
||||
trace('start timer');
|
||||
this.waitInfo = {
|
||||
resolve: null,
|
||||
timer: createWaitTimer(),
|
||||
}
|
||||
this.reparseWaiter = new Promise(resolve => this.waitInfo.resolve = resolve);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from parsing a Java file
|
||||
*/
|
||||
class ParsedInfo {
|
||||
/**
|
||||
* @param {string} uri the file URI
|
||||
* @param {string} content the full file content
|
||||
* @param {number} version the version this parse applies to
|
||||
* @param {Map<string,CEIType>} typemap the set of known types
|
||||
* @param {SourceUnit} unit the parsed unit
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
constructor(uri, content, version, typemap, unit, problems) {
|
||||
this.uri = uri;
|
||||
this.content = content;
|
||||
this.version = version;
|
||||
this.typemap = typemap;
|
||||
this.unit = unit;
|
||||
this.problems = problems;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} uris
|
||||
* @param {Map<string, JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
* @param {{includeMethods: boolean, first_parse?: boolean}} [opts]
|
||||
*/
|
||||
function reparse(uris, liveParsers, androidLibrary, opts) {
|
||||
trace(`reparse`);
|
||||
if (!Array.isArray(uris)) {
|
||||
return;
|
||||
}
|
||||
if (first_parse_waiting) {
|
||||
if (!opts || !opts.first_parse) {
|
||||
// we are waiting for the first parse to complete - add this file to the list
|
||||
uris.forEach(uri => first_parse_waiting.add(uri));
|
||||
trace('waiting for first parse')
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (androidLibrary instanceof Promise) {
|
||||
// reparse after the library has finished loading
|
||||
androidLibrary.then(lib => reparse(uris, liveParsers, lib, opts));
|
||||
return;
|
||||
}
|
||||
|
||||
const cached_units = [], parsers = [];
|
||||
for (let docinfo of liveParsers.values()) {
|
||||
if (uris.includes(docinfo.uri)) {
|
||||
// make a copy of the content + version in case the source file is edited while we're parsing
|
||||
parsers.push({uri: docinfo.uri, content: docinfo.content, version: docinfo.version});
|
||||
} else if (docinfo.parsed) {
|
||||
cached_units.push(docinfo.parsed.unit);
|
||||
}
|
||||
}
|
||||
|
||||
// Each parse uses a unique typemap, initialised from the android library
|
||||
const typemap = new Map(androidLibrary);
|
||||
|
||||
// perform the parse
|
||||
const units = parse(parsers, cached_units, typemap);
|
||||
|
||||
// create new ParsedInfo instances for each of the parsed units
|
||||
units.forEach(unit => {
|
||||
const parser = parsers.find(p => p.uri === unit.uri);
|
||||
if (!parser) return;
|
||||
const doc = liveParsers.get(unit.uri);
|
||||
if (!doc) return;
|
||||
doc.parsed = new ParsedInfo(doc.uri, parser.content, parser.version, typemap, unit, []);
|
||||
});
|
||||
|
||||
let method_body_uris = [];
|
||||
if (first_parse_waiting) {
|
||||
// this is the first parse - parse the bodies of any waiting URIs and
|
||||
// set first_parse_waiting to null
|
||||
method_body_uris = [...first_parse_waiting];
|
||||
first_parse_waiting = null;
|
||||
}
|
||||
|
||||
if (opts && opts.includeMethods) {
|
||||
method_body_uris = uris;
|
||||
}
|
||||
|
||||
if (method_body_uris.length) {
|
||||
time('parse-methods');
|
||||
method_body_uris.forEach(uri => {
|
||||
const doc = liveParsers.get(uri);
|
||||
if (!doc || !doc.parsed) {
|
||||
return;
|
||||
}
|
||||
parseMethodBodies(doc.parsed.unit, typemap);
|
||||
})
|
||||
timeEnd('parse-methods');
|
||||
}
|
||||
}
|
||||
|
||||
exports.indexAt = indexAt;
|
||||
exports.positionAt = positionAt;
|
||||
exports.FileURIMap = FileURIMap;
|
||||
exports.JavaDocInfo = JavaDocInfo;
|
||||
exports.ParsedInfo = ParsedInfo;
|
||||
exports.reparse = reparse;
|
||||
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;
|
||||
}
|
||||
60
langserver/logging.js
Normal file
60
langserver/logging.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { Settings } = require('./settings');
|
||||
|
||||
const earlyTraceBuffer = [];
|
||||
|
||||
/**
|
||||
* Log a trace message with a timestamp - only logs if "Trace enabled" in settings
|
||||
* @param {string} s
|
||||
*/
|
||||
function trace(s) {
|
||||
if (Settings.updateCount > 0 && !Settings.trace) {
|
||||
return;
|
||||
}
|
||||
const msg = `${Date.now()}: ${s}`;
|
||||
// before we've retrieved the trace setting, buffer the messages
|
||||
if (Settings.updateCount === 0) {
|
||||
earlyTraceBuffer.push(msg);
|
||||
return;
|
||||
}
|
||||
if (earlyTraceBuffer.length) {
|
||||
earlyTraceBuffer.splice(0, earlyTraceBuffer.length).forEach(msg => console.log(msg));
|
||||
}
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function info(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of active timers
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const timersRunning = new Set();
|
||||
|
||||
/**
|
||||
* Starts a named timer using `console.time()` - only if "Trace Enabled" in Settings
|
||||
* @param {string} label
|
||||
*/
|
||||
function time(label) {
|
||||
if (Settings.trace) {
|
||||
timersRunning.add(label);
|
||||
console.time(label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a named timer (and prints the elapsed time) using `console.timeEnd()`
|
||||
* @param {string} label
|
||||
*/
|
||||
function timeEnd(label) {
|
||||
if (timersRunning.has(label)) {
|
||||
timersRunning.delete(label);
|
||||
console.timeEnd(label);
|
||||
}
|
||||
}
|
||||
|
||||
exports.info = info;
|
||||
exports.trace = trace;
|
||||
exports.time = time;
|
||||
exports.timeEnd = timeEnd;
|
||||
92
langserver/method-signatures.js
Normal file
92
langserver/method-signatures.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const { Method } = require('java-mti');
|
||||
const { indexAt } = require('./document');
|
||||
const { formatDoc } = require('./doc-formatter');
|
||||
const { trace } = require('./logging');
|
||||
const { event } = require('./analytics');
|
||||
|
||||
let methodsigRequestCount = 0;
|
||||
|
||||
/**
|
||||
* Retrieve method signature information
|
||||
*
|
||||
* Each parsed token that is relevant to a method call is
|
||||
* tagged with the list of possible methods and the best matched
|
||||
* method. The tagged tokens include:
|
||||
* - the opening bracket
|
||||
* - each token in every argument
|
||||
* - each comma between the arguments
|
||||
*
|
||||
* The function locates the nearest non-ws token and checks
|
||||
* for any tagged method-call info. It then converts it
|
||||
* to the relevant vscode method signature structure for display.
|
||||
*
|
||||
* @param {import('vscode-languageserver').SignatureHelpParams} request
|
||||
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
|
||||
*/
|
||||
async function getSignatureHelp(request, liveParsers) {
|
||||
trace('getSignatureHelp');
|
||||
/** @type {import('vscode-languageserver').SignatureHelp} */
|
||||
let sighelp = {
|
||||
signatures: [],
|
||||
activeSignature: 0,
|
||||
activeParameter: 0,
|
||||
}
|
||||
const docinfo = liveParsers.get(request.textDocument.uri);
|
||||
if (!docinfo || !docinfo.parsed) {
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
// wait for any active edits to complete
|
||||
await docinfo.reparseWaiter;
|
||||
|
||||
methodsigRequestCount += 1;
|
||||
if ((methodsigRequestCount === 1) || (methodsigRequestCount === 5) || ((methodsigRequestCount % 25) === 0)) {
|
||||
event('method-sig-requests', { methsig_req_count: methodsigRequestCount });
|
||||
}
|
||||
|
||||
// locate the token at the requested position
|
||||
const index = indexAt(request.position, docinfo.content);
|
||||
const token = docinfo.parsed.unit.getTokenAt(index);
|
||||
if (!token || !token.methodCallInfo) {
|
||||
trace('onSignatureHelp - no method call info');
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
// the token has method information attached to it
|
||||
// - convert it to the required vscode format
|
||||
trace(`onSignatureHelp - ${token.methodCallInfo.methods.length} methods`);
|
||||
sighelp = {
|
||||
signatures: token.methodCallInfo.methods.map(m => {
|
||||
const documentation = formatDoc(`#### ${m.owner.simpleTypeName}${m instanceof Method ? `.${m.name}` : ''}()`, m.docs);
|
||||
const param_docs = new Map();
|
||||
if (documentation) {
|
||||
// extract each of the @param sections (if any)
|
||||
for (let m, re=/@param\s+(\S+)([\d\D]+?)(?=\n\n|\n[ \t*]*@\w+|$)/g; m = re.exec(documentation.value);) {
|
||||
param_docs.set(m[1], m[2]);
|
||||
}
|
||||
}
|
||||
/** @type {import('vscode-languageserver').SignatureInformation} */
|
||||
let si = {
|
||||
label: m.label,
|
||||
documentation,
|
||||
parameters: m.parameters.map(p => {
|
||||
/** @type {import('vscode-languageserver').ParameterInformation} */
|
||||
let pi = {
|
||||
documentation: {
|
||||
kind: 'markdown',
|
||||
value: param_docs.has(p.name) ? `**${p.name}**: ${param_docs.get(p.name)}` : '',
|
||||
},
|
||||
label: p.label
|
||||
}
|
||||
return pi;
|
||||
})
|
||||
}
|
||||
return si;
|
||||
}),
|
||||
activeSignature: token.methodCallInfo.methodIdx,
|
||||
activeParameter: token.methodCallInfo.argIdx,
|
||||
}
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
exports.getSignatureHelp = getSignatureHelp;
|
||||
352
langserver/package-lock.json
generated
Normal file
352
langserver/package-lock.json
generated
Normal file
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "13.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
|
||||
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
|
||||
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.48",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
||||
},
|
||||
"binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
|
||||
"requires": {
|
||||
"buffers": "~0.1.1",
|
||||
"chainsaw": "~0.1.0"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-indexof-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8="
|
||||
},
|
||||
"buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
|
||||
},
|
||||
"chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
|
||||
"requires": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
|
||||
"requires": {
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fstream": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
|
||||
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"inherits": "~2.0.0",
|
||||
"mkdirp": ">=0.5 0",
|
||||
"rimraf": "2"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
|
||||
"requires": {
|
||||
"agent-base": "^4.3.0",
|
||||
"debug": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"java-mti": {
|
||||
"version": "github:adelphes/java-mti#d0e1e45bad4d2bba453dbcb5ad527db023f223e8",
|
||||
"from": "github:adelphes/java-mti#d0e1e45",
|
||||
"requires": {
|
||||
"unzipper": "^0.10.11"
|
||||
}
|
||||
},
|
||||
"listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"mixpanel": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.11.0.tgz",
|
||||
"integrity": "sha512-TS7AkCmfC+vGshlCOjEcITFoFxlt5fdSEqmN+d+pTXAhE5v+jPQW2uUcn9W+Oq4NVXz+kdskU09dsm9vmNl0ig==",
|
||||
"requires": {
|
||||
"https-proxy-agent": "3.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
|
||||
},
|
||||
"unzipper": {
|
||||
"version": "0.10.11",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
|
||||
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
|
||||
"requires": {
|
||||
"big-integer": "^1.6.17",
|
||||
"binary": "~0.3.0",
|
||||
"bluebird": "~3.4.1",
|
||||
"buffer-indexof-polyfill": "~1.0.0",
|
||||
"duplexer2": "~0.1.4",
|
||||
"fstream": "^1.0.12",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"listenercount": "~1.0.1",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "~1.0.4"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
|
||||
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
|
||||
},
|
||||
"vscode-languageserver": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
|
||||
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
|
||||
"requires": {
|
||||
"vscode-languageserver-protocol": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-protocol": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
|
||||
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
|
||||
"requires": {
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageserver-types": "3.15.1"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-textdocument": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
|
||||
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
|
||||
},
|
||||
"vscode-languageserver-types": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
|
||||
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
|
||||
},
|
||||
"vscode-uri": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz",
|
||||
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A=="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
||||
22
langserver/package.json
Normal file
22
langserver/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.3",
|
||||
"description": "Language server for Android development",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"java-mti": "adelphes/java-mti#d0e1e45",
|
||||
"mixpanel": "0.11.0",
|
||||
"uuid": "8.2.0",
|
||||
"vscode-languageserver": "6.1.1",
|
||||
"vscode-languageserver-textdocument": "1.0.1",
|
||||
"vscode-uri": "2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.13.4"
|
||||
}
|
||||
}
|
||||
300
langserver/server.js
Normal file
300
langserver/server.js
Normal file
@@ -0,0 +1,300 @@
|
||||
const {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
ProposedFeatures,
|
||||
DidChangeConfigurationNotification,
|
||||
TextDocumentSyncKind,
|
||||
} = require('vscode-languageserver');
|
||||
const fs = require('fs');
|
||||
|
||||
const { TextDocument } = require('vscode-languageserver-textdocument');
|
||||
const { URI } = require('vscode-uri');
|
||||
|
||||
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
|
||||
const { CEIType } = require('java-mti');
|
||||
|
||||
const { Settings } = require('./settings');
|
||||
const { trace } = require('./logging');
|
||||
const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem } = require('./completions');
|
||||
const { getSignatureHelp } = require('./method-signatures');
|
||||
const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document');
|
||||
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const analytics = require('./analytics');
|
||||
const package_json = require('./package.json');
|
||||
|
||||
/**
|
||||
* The global map of Android system types
|
||||
* @typedef {Map<string, CEIType>} AndroidLibrary
|
||||
* @type {AndroidLibrary|Promise<AndroidLibrary>}
|
||||
*/
|
||||
let androidLibrary = null;
|
||||
|
||||
/**
|
||||
* The list of loaded Java documents
|
||||
* @type {Map<string,JavaDocInfo>}
|
||||
*/
|
||||
const liveParsers = new FileURIMap();
|
||||
|
||||
let startupOpts = null;
|
||||
let hasConfigurationCapability = false;
|
||||
let hasWorkspaceFolderCapability = false;
|
||||
|
||||
function loadCodeCompletionLibrary(extensionPath, codeCompletionLibraries) {
|
||||
// the android library is loaded asynchronously, with the global `androidLibrary` variable
|
||||
// set to the promise while it is loading.
|
||||
androidLibrary = (androidLibrary instanceof Promise
|
||||
? androidLibrary // if we're currently loading, wait for it to complete
|
||||
: Promise.resolve(new Map())
|
||||
)
|
||||
.then(() => loadAndroidSystemLibrary(extensionPath, codeCompletionLibraries))
|
||||
.then(
|
||||
library => androidLibrary = library,
|
||||
err => {
|
||||
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
|
||||
return new Map();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Text document manager monitoring file opens and edits
|
||||
let documents = new TextDocuments({
|
||||
/**
|
||||
*
|
||||
* @param {string} uri
|
||||
* @param {string} languageId
|
||||
* @param {number} version
|
||||
* @param {string} content
|
||||
*/
|
||||
create(uri, languageId, version, content) {
|
||||
trace(`document create ${uri}:${version}`);
|
||||
|
||||
// sanity-check - we only support Java source files
|
||||
if (!/\.java$/i.test(uri)) {
|
||||
return { uri };
|
||||
}
|
||||
|
||||
// add the document to the set
|
||||
liveParsers.set(uri, new JavaDocInfo(uri, content, version));
|
||||
|
||||
// tokenize the file content and build the initial parse state
|
||||
reparse([uri], liveParsers, androidLibrary, { includeMethods: true });
|
||||
|
||||
return { uri };
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {TextDocument} document
|
||||
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
|
||||
* @param {number} version
|
||||
*/
|
||||
update(document, changes, version) {
|
||||
trace(`document update ${document.uri}:${version}`);
|
||||
if (!liveParsers.has(document.uri)) {
|
||||
return;
|
||||
}
|
||||
const docinfo = liveParsers.get(document.uri);
|
||||
if (!docinfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply the edits to our local content copy
|
||||
changes.forEach((change) => {
|
||||
/** @type {import('vscode-languageserver').Range} */
|
||||
const r = change['range'];
|
||||
if (r) {
|
||||
const start_index = indexAt(r.start, docinfo.content);
|
||||
let end_index = start_index + (r.end.character - r.start.character);
|
||||
if (r.end.line !== r.start.line) end_index = indexAt(r.end, docinfo.content);
|
||||
docinfo.content = `${docinfo.content.slice(0, start_index)}${change.text}${docinfo.content.slice(end_index)}`;
|
||||
}
|
||||
});
|
||||
|
||||
docinfo.version = version;
|
||||
docinfo.scheduleReparse(liveParsers, androidLibrary);
|
||||
|
||||
return document;
|
||||
},
|
||||
});
|
||||
|
||||
// Create a connection for the server. The connection uses Node's IPC as a transport.
|
||||
const connection = createConnection(ProposedFeatures.all);
|
||||
|
||||
connection.onInitialize((params) => {
|
||||
|
||||
startupOpts = {
|
||||
extensionPath: '',
|
||||
initialSettings: {
|
||||
appSourceRoot: '',
|
||||
/** @type {string[]} */
|
||||
codeCompletionLibraries: [],
|
||||
trace: false,
|
||||
},
|
||||
sourceFiles: [],
|
||||
...params.initializationOptions,
|
||||
}
|
||||
|
||||
Settings.set(startupOpts.initialSettings);
|
||||
analytics.init(undefined, startupOpts.mpuid, uuidv4(), package_json, { vscode_version: startupOpts.vscodeVersion });
|
||||
|
||||
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
|
||||
|
||||
let capabilities = params.capabilities;
|
||||
|
||||
// Does the client support the `workspace/configuration` request?
|
||||
// If not, we will fall back using global settings
|
||||
hasConfigurationCapability = capabilities.workspace && !!capabilities.workspace.configuration;
|
||||
|
||||
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
|
||||
|
||||
/** @type {string[]} */
|
||||
const file_uris = Array.isArray(startupOpts.sourceFiles) ? startupOpts.sourceFiles : [];
|
||||
for (let file_uri of file_uris) {
|
||||
const file = URI.parse(file_uri, true);
|
||||
const filePath = file.fsPath;
|
||||
if (!/.java/i.test(filePath)) {
|
||||
trace(`ignoring non-java file: ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
if (liveParsers.has(file_uri)) {
|
||||
trace(`File already loaded: ${file_uri}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// it's fine to load the initial file set synchronously - the language server runs in a
|
||||
// separate process and nothing (useful) can happen until the first parse is complete.
|
||||
const content = fs.readFileSync(file.fsPath, 'utf8');
|
||||
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
|
||||
trace(`Added initial file: ${file_uri}`);
|
||||
} catch (err) {
|
||||
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
|
||||
}
|
||||
}
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
|
||||
|
||||
return {
|
||||
capabilities: {
|
||||
// we support incremental updates
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
|
||||
// we support code completion
|
||||
completionProvider: {
|
||||
resolveProvider: true,
|
||||
},
|
||||
|
||||
// we support method signature information
|
||||
signatureHelpProvider : {
|
||||
triggerCharacters: [ '(' ]
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
connection.onInitialized(async () => {
|
||||
if (hasConfigurationCapability) {
|
||||
// Register for all configuration changes.
|
||||
connection.client.register(
|
||||
DidChangeConfigurationNotification.type, {
|
||||
section: 'android-dev-ext',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
|
||||
trace('Workspace folder change event received.');
|
||||
});
|
||||
}
|
||||
|
||||
trace('Initialization complete');
|
||||
});
|
||||
|
||||
connection.onDidChangeConfiguration(async (change) => {
|
||||
trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`);
|
||||
|
||||
const prev_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
|
||||
|
||||
// fetch and update the settings
|
||||
const newSettings = await connection.workspace.getConfiguration({
|
||||
section: "android-dev-ext"
|
||||
});
|
||||
|
||||
Settings.set(newSettings);
|
||||
|
||||
if (Settings.updateCount > 2) {
|
||||
analytics.event('ls-settings-changed', {
|
||||
appSourceRoot: Settings.appSourceRoot,
|
||||
libs: Settings.codeCompletionLibraries,
|
||||
trace: Settings.trace,
|
||||
})
|
||||
}
|
||||
|
||||
const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
|
||||
if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) {
|
||||
// code-completion libraries have changed - reload the android library
|
||||
trace("code completion libraries changed - reloading android library and reparsing")
|
||||
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false });
|
||||
clearDefaultCompletionEntries();
|
||||
}
|
||||
})
|
||||
|
||||
documents.onDidClose((e) => {
|
||||
trace(`doc closed ${e.document.uri}`);
|
||||
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
|
||||
});
|
||||
|
||||
connection.onDidChangeWatchedFiles(
|
||||
/** @param {import('vscode-languageserver').DidChangeWatchedFilesParams} params */
|
||||
(params) => {
|
||||
// Monitored files have change in VS Code
|
||||
trace(`watch file change: ${JSON.stringify(params)}`);
|
||||
let files_changed = false;
|
||||
params.changes.forEach(change => {
|
||||
switch(change.type) {
|
||||
case 1: // create
|
||||
// if the user creates the file directly in vscode, the file will automatically open (and we receive an open callback)
|
||||
// - but if the user creates or copies a file into the workspace, we need to manually add it to the set.
|
||||
if (!liveParsers.has(change.uri)) {
|
||||
trace(`file added: ${change.uri}`)
|
||||
try {
|
||||
const fname = URI.parse(change.uri, true).fsPath;
|
||||
liveParsers.set(change.uri, new JavaDocInfo(change.uri, fs.readFileSync(fname, 'utf8'), 0));
|
||||
files_changed = true;
|
||||
} catch (err) {
|
||||
console.log(`Failed to add new file '${change.uri}' to working set. ${err.message}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // change
|
||||
// called when the user manually saves the file - ignore for now
|
||||
break;
|
||||
case 3: // delete
|
||||
trace(`file deleted: ${change.uri}`)
|
||||
liveParsers.delete(change.uri);
|
||||
files_changed = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (files_changed) {
|
||||
// reparse the entire set
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Retrieve the initial list of the completion items.
|
||||
connection.onCompletion(params => getCompletionItems(params, liveParsers, androidLibrary));
|
||||
|
||||
// Resolve additional information for the item selected in the completion list.
|
||||
connection.onCompletionResolve(item => resolveCompletionItem(item));
|
||||
|
||||
// Retrieve method signature information
|
||||
connection.onSignatureHelp(params => getSignatureHelp(params, liveParsers));
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
42
langserver/settings.js
Normal file
42
langserver/settings.js
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
const defaultSettings = {
|
||||
appSourceRoot: 'app/src/main',
|
||||
codeCompletionLibraries: [],
|
||||
trace: false,
|
||||
}
|
||||
|
||||
class AndroidProjectSettings {
|
||||
/**
|
||||
* The root of the app source folder.
|
||||
* This folder should contain AndroidManifest.xml as well as the asets, res, etc folders
|
||||
*/
|
||||
appSourceRoot = defaultSettings.appSourceRoot;
|
||||
|
||||
/**
|
||||
* The set of androidx libraries to include in code completion
|
||||
*/
|
||||
codeCompletionLibraries = defaultSettings.codeCompletionLibraries;
|
||||
|
||||
/**
|
||||
* True if we log details
|
||||
*/
|
||||
trace = defaultSettings.trace;
|
||||
|
||||
updateCount = 0;
|
||||
|
||||
static Instance = new AndroidProjectSettings();
|
||||
|
||||
set(values) {
|
||||
if (!values || typeof values !== 'object') {
|
||||
return;
|
||||
}
|
||||
this.updateCount += 1;
|
||||
for (let key in defaultSettings) {
|
||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
||||
this[key] = values[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Settings = AndroidProjectSettings.Instance;
|
||||
98
langserver/tests/test-tokenizer.js
Normal file
98
langserver/tests/test-tokenizer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { tokenize } = require('../java/tokenizer');
|
||||
|
||||
function testTokenize() {
|
||||
const tests = [
|
||||
// the basics
|
||||
{ src: 'i', r: [{value: 'i', kind:'ident'}] },
|
||||
{ src: '0', r: [{value: '0', kind:'int-number-literal'}] },
|
||||
{ src: `""`, r: [{value: `""`, kind:'string-literal'}] },
|
||||
{ src: `'x'`, r: [{value: `'x'`, kind:'char-literal'}] },
|
||||
{ src: `(`, r: [{value: `(`, kind:'open-bracket'}] },
|
||||
...'. , [ ] ? : @'.split(' ').map(symbol => ({ src: symbol, r: [{value: symbol, kind: 'symbol'}] })),
|
||||
...'= += -= *= /= %= >>= <<= &= |= ^='.split(' ').map(op => ({ src: op, r: [{value: op, kind:'assignment-operator'}] })),
|
||||
...'+ -'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'plumin-operator'}] })),
|
||||
...'* / %'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'muldiv-operator'}] })),
|
||||
...'# ¬'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'invalid'}] })),
|
||||
|
||||
// numbers - decimal with exponent
|
||||
...'0.0e+0 0.0E+0 0e+0 0e0 .0e0 0e0f 0e0d'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
|
||||
// numbers - decimal with partial exponent
|
||||
...'0.0e+ 0.0E+ 0e+ 0e .0e 0ef 0ed'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
|
||||
// numbers - not decimal exponent
|
||||
{ src: '0.0ea', r: [{value: '0.0e', kind:'dec-exp-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
|
||||
// numbers - decimal (no exponent)
|
||||
...'0.123 0. 0.f 0.0D .0 .0f .123D'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-number-literal'}] })),
|
||||
// numbers - not decimal
|
||||
{ src: '0.a', r: [{value: '0.', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
{ src: '0.0a', r: [{value: '0.0', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
|
||||
// numbers - hex
|
||||
...'0x0 0x123456789abcdef 0xABCDEF 0xabcdefl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
|
||||
// numbers - partial hex
|
||||
...'0x 0xl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
|
||||
|
||||
// numbers - decimal
|
||||
...'0 123456789 0l'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'int-number-literal'}] })),
|
||||
|
||||
// strings
|
||||
...[`"abc"`, `"\\n"`, `"\\""`].map(num => ({ src: num, r: [{value: num, kind:'string-literal'}] })),
|
||||
// unterminated strings
|
||||
...[`"abc`, `"\\n`, `"\\"`, `"`].map(num => ({ src: num, r: [{value: num, kind:'unterminated-string-literal'}] })),
|
||||
// strings cannot cross newlines
|
||||
{ src: `"abc\n`, r: [{value: `"abc`, kind:'unterminated-string-literal'}, {value: '\n', kind:'wsc'}] },
|
||||
|
||||
// characters
|
||||
...[`'a'`, `'\\n'`, `'\\''`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
|
||||
// unterminated/invalid characters
|
||||
...[`'a`, `'\\n`, `'\\'`, `''`, `'`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
|
||||
// characters cannot cross newlines
|
||||
{ src: `'\n`, r: [{value: `'`, kind:'char-literal'}, {value: '\n', kind:'wsc'}] },
|
||||
|
||||
// arity symbol
|
||||
{ src: `int...x`, r: [
|
||||
{value: `int`, kind:'primitive-type'},
|
||||
{value: `...`, kind:'symbol'},
|
||||
{value: `x`, kind:'ident'},
|
||||
],},
|
||||
|
||||
// complex inc - the javac compiler doesn't bother to try and sensibly separate +++ - it just appears to
|
||||
// prioritise ++ in every case, assuming that the developer will insert spaces as required.
|
||||
// e.g this first one fails to compile with javac
|
||||
{ src: '++abc+++def', r: [
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'abc', kind:'ident'},
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: '+', kind:'plumin-operator'},
|
||||
{value: 'def', kind:'ident'},
|
||||
] },
|
||||
// this should be ok
|
||||
{ src: '++abc+ ++def', r: [
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'abc', kind:'ident'},
|
||||
{value: '+', kind:'plumin-operator'},
|
||||
{value: ' ', kind:'wsc'},
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'def', kind:'ident'},
|
||||
] },
|
||||
]
|
||||
const report = (test, msg) => {
|
||||
console.log(JSON.stringify({test, msg}));
|
||||
}
|
||||
tests.forEach(t => {
|
||||
const tokens = tokenize(t.src);
|
||||
if (tokens.length !== t.r.length) {
|
||||
report(t, `Wrong token count. Expected ${t.r.length}, got ${tokens.length}`);
|
||||
return;
|
||||
}
|
||||
for (let i=0; i < tokens.length; i++) {
|
||||
if (tokens[i].value !== t.r[i].value)
|
||||
report(t, `Wrong token value. Expected ${t.r[i].value}, got ${tokens[i].value}`);
|
||||
if (tokens[i].kind !== t.r[i].kind)
|
||||
report(t, `Wrong token kind. Expected ${t.r[i].kind}, got ${tokens[i].kind}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
testTokenize();
|
||||
Reference in New Issue
Block a user