Files
android-dev-ext/langserver/java/type-resolver.js
2020-06-08 15:52:03 +01:00

366 lines
13 KiB
JavaScript

/**
* @typedef {Map<string,JavaType>} TypeMap
*/
const { JavaType, PrimitiveType, ArrayType, CEIType } = require('java-mti');
const { ResolvedImport } = require('./import-resolver');
const ResolvedType = require('./parsetypes/resolved-type');
/**
* Parse a type into its various components
* @param {string} label
* @returns {{type:ResolvedType, error:string}}
*/
function parse_type(label) {
const type = new ResolvedType();
let re = /([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)|(\.[a-zA-Z_]\w*)|[<,>]|((?:\[\])+)|( +)|./g;
let parts = [type.addTypePart()];
for (let m; m = re.exec(label);) {
if (m[4]) {
// ignore ws
continue;
}
if (!parts[0].name) {
if (m[1]) {
parts[0].name = m[1];
continue;
}
return { type, error: 'Missing type identifier' };
}
if (m[0] === '<') {
if (!parts[0].typeargs && !parts[0].owner.arrdims) {
// start of type arguments - start a new type
const t = new ResolvedType(true);
parts[0].typeargs = [t];
parts.unshift(t.addTypePart());
continue;
}
return { type, error: `Unexpected '<' character` };
}
if (m[0] === ',') {
if (parts[1] && parts[1].typeargs) {
// type argument separator - replace the type on the stack
const t = new ResolvedType(true);
parts[1].typeargs.push(t);
parts[0] = t.addTypePart();
continue;
}
return { type, error: `Unexpected ',' character` };
}
if (m[0] === '>') {
if (parts[1] && parts[1].typeargs) {
// end of type arguments
parts.shift();
continue;
}
return { type, error: `Unexpected '>' character` };
}
if (m[2]) {
if (parts[0].typeargs || parts[0].outer) {
// post-type-args enclosed type
parts[0] = parts[0].inner = parts[0].owner.addTypePart(m[2].slice(1), parts[0]);
continue;
}
return { type, error: `Unexpected '.' character` };
}
if (m[3]) {
parts[0].owner.arrdims = m[3].length / 2;
continue;
}
return { type, error: `Invalid type` };
}
if (parts.length !== 1) {
// one or more missing >
return { type, error: `Missing >` };
}
return { type, error: '' };
}
/**
* Construct a regex to search for an enclosed type in the current and outer scopes of a given type
*
* @param {string} fully_qualified_scope the JRE name (a.b.X$Y) of the current type scope
* @param {string} dotted_raw_typename the dotted name of the type we are searching for
*/
function createTypeScopeRegex(fully_qualified_scope, dotted_raw_typename) {
// split the type name across enclosed type boundaries
const scopes = fully_qualified_scope.split('$');
// if the typename we are searching represents an enclosed type, the type-qualifier dots must be replaced with $
const enclosed_raw_typename = dotted_raw_typename.replace(/\./g,'[$]');
// bulld up the list of possible type matches based upon each outer scope of the type
const enclosed_type_regexes = [];
while (scopes.length) {
enclosed_type_regexes.push(`${scopes.join('[$]')}[$]${enclosed_raw_typename}`);
scopes.pop();
}
// the final regex is an exact match of possible type names, sorted from inner scope to outer (top-level) scope
return new RegExp(`^(${enclosed_type_regexes.join('|')})$`);
}
/**
* Locate JavaTypes that match a type label.
* @param {string} type_label The type to resolve
* @param {string} fully_qualified_scope The fully-qualified JRE name of the current type scope.
* @param {ResolvedImport[]} resolved_imports The list of types resolved from the imports
* @param {TypeMap} typemap
*/
function resolveType(type_label, fully_qualified_scope, resolved_imports, typemap) {
const { type, error } = parse_type(type_label);
if (error) {
// don't try to find the type if the parsing failed
type.error = error;
return type;
}
// locate the JavaTypes for the type and type arguments
resolveCompleteType(type, fully_qualified_scope, resolved_imports, typemap);
return type;
}
/**
*
* @param {ResolvedType} type
* @param {string} fully_qualified_scope
* @param {ResolvedImport[]} resolved_imports
* @param {TypeMap} typemap
*/
function resolveCompleteType(type, fully_qualified_scope, resolved_imports, typemap) {
type.mtis = findJavaTypes(type.getDottedRawType(), type.arrdims, fully_qualified_scope, resolved_imports, typemap);
// resolve type arguments
type.parts.filter(p => p.typeargs).forEach(p => {
p.typeargs.forEach(typearg => {
resolveCompleteType(typearg, fully_qualified_scope, resolved_imports, typemap);
})
})
}
/**
* @param {string} dotted_raw_typename
* @param {number} arraydims
* @param {string} fully_qualified_scope The fully-qualified JRE name of the current type scope.
* @param {ResolvedImport[]} resolved_imports The list of types resolved from the imports
* @param {TypeMap} typemap
*/
function findJavaTypes(dotted_raw_typename, arraydims, fully_qualified_scope, resolved_imports, typemap) {
let types = findRawJavaTypes(dotted_raw_typename, fully_qualified_scope, resolved_imports, typemap);
if (arraydims > 0) {
// convert matches to array types
const array_types = types.map(t => new ArrayType(t, arraydims));
return array_types;
}
return types;
}
/**
* Match a dotted type name to one or more JavaTypes
* @param {string} dotted_raw_typename
* @param {string} fully_qualified_scope The fully-qualified JRE name of the current type scope.
* @param {TypeMap} typemap
* @param {ResolvedImport[]} resolved_imports The list of types resolved from the imports
* @returns {(PrimitiveType|CEIType)[]}
*/
function findRawJavaTypes(dotted_raw_typename, fully_qualified_scope, resolved_imports, typemap) {
// first check if it's a simple primitive
if (PrimitiveType.isPrimitiveTypeName(dotted_raw_typename)) {
// return the primitive type
return [PrimitiveType.fromName(dotted_raw_typename)];
}
// create a regex to search for the type name
// - the first search is for exact type matches inside the current type scope (and any parent type scopes)
let search = createTypeScopeRegex(fully_qualified_scope, dotted_raw_typename);
let matched_types =
resolved_imports.map(ri => ({
ri,
mtis: ri.fullyQualifiedNames.filter(fqn => search.test(fqn)).map(fqn => ri.types.get(fqn))
}))
.filter(x => x.mtis.length);
if (!matched_types.length) {
// if the type was not found in the current type scope, construct a new search for the imported types.
// - since we don't know if the type name includes package qualifiers or not, this regex allows for implicit
// package prefixes (todo - need to figure out static type imports)
search = new RegExp(`^(.+?/)?${dotted_raw_typename.replace(/\./g,'[/$]')}$`);
// search the imports for the type
matched_types =
resolved_imports.map(ri => ({
ri,
mtis: ri.fullyQualifiedNames.filter(fqn => search.test(fqn)).map(fqn => ri.types.get(fqn))
}))
.filter(x => x.mtis.length);
}
// if the type matches multiple import entries, exact imports take prioirity over demand-load imports
let exact_import_matches = matched_types.filter(x => x.ri.import && !x.ri.import.isDemandLoad);
if (exact_import_matches.length) {
if (exact_import_matches.length < matched_types.length) {
matched_types = exact_import_matches;
}
}
if (!matched_types.length) {
// if the type doesn't match any import, the final option is a fully qualified match across all types in all libraries
search = new RegExp(`^${dotted_raw_typename.replace(/\./g,'[/$]')}$`);
for (let typename of typemap.keys()) {
if (search.test(typename)) {
matched_types = [{
ri: null,
mtis: [typemap.get(typename)]
}];
break;
}
}
}
// at this point, we should (hopefully) have a single matched type
// - if the matched_types array is empty, the type is not found
// - if the matched_type array has more than one entry, the type matches types across multiple imports
// - if the matched_type array has one entry and multiple MTIs, the type matches multiple types in a single import
return matched_types
.reduce((types, type) => [...types, ...type.mtis] , [])
}
/**
* Converts an array of type name strings to resolved types
* @param {string[]} types
* @param {string} fully_qualified_scope the JRE name of the type scope we are resolving in
* @param {ResolvedImport[]} resolved_imports the list of resolved imports (and types associated with them)
* @param {TypeMap} typemap
*/
function resolveTypes(types, fully_qualified_scope, resolved_imports, typemap) {
return types.map(typename => resolveType(typename, fully_qualified_scope, resolved_imports, typemap));
}
/**
* Converts an array of TypeIdent instances to resolved types
* @param {import('./parsetypes/typeident')[]} types
* @param {string} fully_qualified_scope the JRE name of the type scope we are resolving in
* @param {ResolvedImport[]} resolved_imports the list of resolved imports (and types associated with them)
* @param {TypeMap} typemap
*/
function resolveTypeIdents(types, fully_qualified_scope, resolved_imports, typemap) {
const names = types.map(typeident =>
typeident.tokens.map(token => token.text).join('')
);
return resolveTypes(names, fully_qualified_scope, resolved_imports, typemap);
}
/**
*
* @param {string} ident
* @param {CEIType} scoped_type
* @param {ResolvedImport[]} imports
* @param {Map<string,JavaType>} typemap
*/
function resolveTypeOrPackage(ident, scoped_type, imports, typemap) {
const types = [];
let package_name = '';
// is it an enclosed type of the currently scoped type or any outer type
if (scoped_type) {
const scopes = scoped_type.shortSignature.split('$');
while (scopes.length) {
const enc_type = typemap.get(`${scopes.join('$')}$${ident}`);
if (enc_type) {
types.push(enc_type);
break;
}
scopes.pop();
}
}
if (!types[0]) {
// is it a top-level type from the imports
const top_level_type = '/' + ident;
for (let i of imports) {
const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(top_level_type));
if (fqn) {
types.push(i.types.get(fqn));
}
}
}
if (!types[0]) {
// is it a default-package type
const default_type = typemap.get(ident);
if (default_type) {
types.push(default_type);
}
}
// the final option is the start of a package name
const package_root = ident + '/';
const typelist = [...typemap.keys()];
if (typelist.find(fqn => fqn.startsWith(package_root))) {
package_name = ident;
}
return {
types,
package_name,
}
}
/**
*
* @param {string} ident
* @param {JavaType[]} outer_types
* @param {string} outer_package_name
* @param {Map<string,JavaType>} typemap
*/
function resolveNextTypeOrPackage(ident, outer_types, outer_package_name, typemap) {
const types = [];
let package_name = '';
outer_types.forEach(type => {
if (type instanceof CEIType) {
const enclosed_type_signature = `${type.shortSignature}$${ident}`;
const enclosed_type = typemap.get(enclosed_type_signature);
if (enclosed_type) {
// it matches an inner/enclosed type
types.push(enclosed_type);
}
}
})
if (outer_package_name) {
const type_match = `${outer_package_name}/${ident}`;
if (typemap.has(type_match)) {
// it matches a type
types.push(typemap.get(type_match));
}
const package_match = type_match + '/';
if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) {
// it matches a sub-package
package_name = type_match;
}
}
return {
types,
package_name,
}
}
module.exports = {
parse_type,
resolveType,
resolveTypes,
resolveTypeIdents,
ResolvedType,
resolveTypeOrPackage,
resolveNextTypeOrPackage,
}