diff --git a/langserver/java/import-resolver.js b/langserver/java/import-resolver.js index 51581e3..abd9921 100644 --- a/langserver/java/import-resolver.js +++ b/langserver/java/import-resolver.js @@ -12,9 +12,9 @@ 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') + ? 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'); + : new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}$`, 'gm'); // run the regex against the list of type names const matching_names = typenames.match(matcher); @@ -37,23 +37,20 @@ function resolveImportTypes(typenames, import_decl) { * - followed by import declarations (in order of declaration), * - followed by implicit packages * - * @param {*} androidLibrary imported types from the Android platform library + * @param {Map} androidLibrary + * @param {import('java-mti').JavaType[]} sourceTypes * @param {ImportBlock[]} imports list of declared imports in the module * @param {string} package_name package name of the module - * @param {import('./mti').Type[]} source_mtis MTIs representing types declared in the source * @param {string[]} [implicitPackages] list of implicit demand-load packages */ -function resolveImports(androidLibrary, imports, package_name, source_mtis, implicitPackages = ['java.lang']) { - /** - * create a new Map that maps JRE type names to MTI instances - * @type {Map} - */ - const typemap = new Map( - androidLibrary.types.map(mti => [`${mti.package}.${mti.name}`, mti]) - ); - // add the source MTIs - // todo - should we overwrite entries when source MTIs match types in the library? - source_mtis.forEach(mti => typemap.set(`${mti.package}.${mti.name}`, mti)) +function resolveImports(androidLibrary, sourceTypes, imports, package_name, implicitPackages = ['java.lang']) { + + const typemap = new Map(androidLibrary); + + sourceTypes.forEach(t => { + // todo - should we overwrite entries when source types match types in the library? + typemap.set(t.shortSignature, t); + }) // construct the list of typenames const typenames = [...typemap.keys()].join('\n'); diff --git a/langserver/java/mti.js b/langserver/java/mti.js deleted file mode 100644 index 07e60bc..0000000 --- a/langserver/java/mti.js +++ /dev/null @@ -1,1018 +0,0 @@ - -/** - * @param {number} ref - * @param {MTI} mti - * @returns {string} - */ -function packageNameFromRef(ref, mti) { - if (typeof ref !== 'number') { - return null; - } - if (ref < 16) { - return KnownPackages[ref]; - } - return mti.minified.rp[ref - 16]; -} - -/** - * @param {number} ref - * @param {MTI} unit - */ -function typeFromRef(ref, unit) { - if (typeof ref !== 'number') { - return null; - } - if (ref < 16) { - return KnownTypes[ref]; - } - return unit.referenced.types[ref - 16]; -} - -function indent(s) { - return '\n' + s.split('\n').map(s => ` ${s}`).join('\n'); -} - -/** - * @typedef {MTIType|MTIArrayType|MTIPrimitiveType} Type - * @typedef {'class'|'interface'|'enum'|'@interface'|'primitive'|'array'} MTITypeKind - */ - -class MinifiableInfo { - - constructor(minified) { - this.minified = minified; - } - - /** - * Format a commented form of docs with a newline at the end. - */ - fmtdocs() { - // the docs field is always d in the minified objects - const d = this.minified.d; - return d ? `/**\n * ${d.replace(/\n/g,'\n *')}\n */\n` : ''; - } -} - -/** - * Minified Type Information - * - * Each MTI instance represents a Java unit (a single source file or a compiled class file). - * The mti JSON format is minimalistic to keep the size small - the Android framework has over 8000 classes in - * it, so keeping the information as small as possible is beneficial. - * ``` - mti: { - rp:[], // referenced packages - rt:[], // referenced types - it:[{ // implemented types - m:0, // type modifiers - n:'', // type name (in X$Y format) - p:null, // owner package - v:[], // type vars - e:null, // extends 0(class)/[0,...](interface)/null(unknown) - i:[], // implements [0,...] - c:[], // constructors [{m:0,p:[]},...] - f:[], // fields {m:0,n:'',t:0}, - g:[], // methods {n:'',s:[{m:0,t:0,p:[{m:0,t:0,n:''},...]}]} - u:[], // subtypes [0,...] - d:'', // type docs - }] - }, - ``` - */ -class MTI extends MinifiableInfo { - /** - * @param {string} package_name - * @param {string} docs - * @param {string[]} modifiers - * @param {'class'|'enum'|'interface'|'@interface'} typeKind - * @param {string} name - * @param {string[]} typeVarNames - */ - addType(package_name, docs, modifiers, typeKind, name, typeVarNames) { - const t = { - d: docs, - p: this.addPackage(package_name), - m: getTypeMods(modifiers, typeKind), - n: name.replace(/\./g,'$'), - v: typeVarNames.map(name => this.addRefType('', name)), - e: /interface/.test(typeKind) ? [] - : typeKind === 'enum' ? this.addRefType('java.lang', 'Enum') - : this.addRefType('java.lang', 'Object'), - i: [], - f: [], - c: [], - g: [], - } - this.minified.it.push(t); - const mtitype = new MTIType(this, t); - this.types.push(mtitype); - return mtitype; - } - - /** - * @param {number} base_typeref - * @param {number[]} type_args - */ - addGenericRefType(base_typeref, type_args) { - const targs_key = type_args.join(','); - let idx = this.minified.rt.findIndex(t => (t.n === base_typeref) && !t.a && t.g && (t.g.join(',') === targs_key)); - if (idx < 0) { - const rt_mti = { - n: base_typeref, - g: type_args, - }; - idx = this.minified.rt.push(rt_mti) - 1; - this.referenced.types.push(new ReferencedType(this, rt_mti)); - } - return idx + 16; - } - - addArrayRefType(element_typeref, dimensions) { - let idx = this.minified.rt.findIndex(t => (t.n === element_typeref) && !t.g && (t.a === dimensions)); - if (idx < 0) { - const rt_mti = { - n: element_typeref, - a: dimensions, - }; - idx = this.minified.rt.push(rt_mti) - 1; - this.referenced.types.push(new ReferencedType(this, rt_mti)); - } - return idx + 16; - } - - /** - * @param {string} package_name - * @param {string} type_name - */ - addRefType(package_name, type_name) { - let idx; - if (!package_name || package_name === 'java.lang') { - idx = KnownTypes.findIndex(t => t.name === type_name); - if (idx >= 0) { - return idx; - } - } - const pkgref = this.addPackage(package_name); - const jre_type_name = type_name.replace(/\./g, '$'); - idx = this.minified.rt.findIndex(t => t.p === pkgref && t.n === jre_type_name); - if (idx < 0) { - const rt_mti = { - p: pkgref, - n: jre_type_name, - }; - idx = this.minified.rt.push(rt_mti) - 1; - this.referenced.types.push(new ReferencedType(this, rt_mti)) - } - return idx + 16; - } - - /** - * @param {string} packagename - */ - addPackage(packagename) { - let idx = KnownPackages.indexOf(packagename); - if (idx >= 0) { - return idx; - } - idx = this.minified.rp.indexOf(packagename); - if (idx < 0) { - idx = this.minified.rp.push(packagename) - 1; - } - return idx + 16; - } - - static get defaultPackageRef() { - return KnownPackages.indexOf(""); - } - - /** - * @param {string} name - */ - static fromPrimitive(name) { - return MTIPrimitiveType.fromName(name); - } - - /** - * @param {Type} element - */ - static makeArrayType(element, dimensions) { - let res = element; - for (let i = 0; i < dimensions; i++) { - res = new MTIArrayType(res); - } - return res; - } - - /** - * @param {{rp:string[], rt:*[], it:*[]}} mti - */ - constructor(mti = {rp:[],rt:[],it:[]}) { - super(mti); - // initialise the lists of referenced packages and types - this.referenced = { - /** @type {string[]} */ - packages: mti.rp, - - /** @type {ReferencedType[]} */ - types: [], - } - // because ReferencedType can make use of earlier reference types, we must add them sequentially - // instead of using mti.rt.map() - for (let t of mti.rt) { - this.referenced.types.push(new ReferencedType(this, t)) - } - - // add the types implemented by this unit - this.types = mti.it.map(it => new MTIType(this, it)); - } - - /** - * Unpack all the classes from the given JSON - * @param {string} filename - */ - static unpackJSON(filename) { - const o = JSON.parse(require('fs').readFileSync(filename, 'utf8')); - delete o.NOTICES; - const types = []; - for (let pkg in o) { - for (let cls in o[pkg]) { - const unit = new MTI(o[pkg][cls]); - types.push(...unit.types); - } - } - return { - packages: Object.keys(o).sort(), - types: types.sort((a,b) => a.minified.n.localeCompare(b.minified.n)), - } - } -} - -/** - * A ReferencedType encodes a type used by a class, interface or enum. - * ``` - * { - * n: string | typeref - name or base typeref (for arrays and generic types) - * p?: pkgref - package the type is declared in (undefined for primitives) - * g?: typeref[] - generic type parameters - * a?: number - array dimensions - * } - * ``` - * - * A typeref value < 16 is a lookup into the KnownTypes array. - * - * All other types have a typeref >= 16 and an associated package reference. - * - * The packageref is a lookup into the MTIs pt array which lists package names. - */ -class ReferencedType extends MinifiableInfo { - - /** - * @param {MTI} unit - * @param {*} mti - * @param {string|false} [pkg_or_prim] predefined package name, an empty string for default packages or false for primitives - * @param {*} [default_value] - */ - constructor(unit, mti, pkg_or_prim, default_value = null) { - super(mti); - let baseType; - if (typeof mti.n === 'number') { - baseType = typeFromRef(mti.n, unit); - } - this.parsed = { - package: pkg_or_prim - || ((pkg_or_prim === false) - ? undefined - : packageNameFromRef(mti.p, unit) - ), - - /** @type {ReferencedType} */ - baseType, - - /** @type {ReferencedType[]} */ - typeArgs: mti.g && mti.g.map(t => typeFromRef(t, unit)), - - /** @type {string} */ - arr: '[]'.repeat(mti.a | 0), - } - this.defaultValue = default_value; - } - - get isPrimitive() { return this.parsed.package === undefined } - - get package() { return this.parsed.package } - - get name() { - // note: names in enclosed types are in x$y format - const n = this.parsed.baseType ? this.parsed.baseType.name : this.minified.n; - const type_args = this.parsed.typeArgs - ? `<${this.parsed.typeArgs.map(tp => tp.name).join(',')}>` - : '' - return `${n}${type_args}${this.parsed.arr}`; - } - - get dottedName() { - return this.name.replace(/[$]/g, '.'); - } -} - -class MTITypeBase extends MinifiableInfo { - /** - * type docs - * @type {string} - */ - get docs() { return this.minified.d } - - /** - * type modifiers - * @type {number} - */ - get modifiers() { return this.minified.m } - - /** - * type name (in x$y format for enclosed types) - * @type {string} - */ - get name() { return this.minified.n } - - /** - * package this type belongs to - */ - get package() { return null } - - /** - * @type {MTIConstructor[]} - */ - get constructors() { return [] } - - /** - * @type {MTIField[]} - */ - get fields() { return [] } - - /** - * @type {MTIMethod[]} - */ - get methods() { return [] } - - /** - * @type {ReferencedType[]} - */ - get typevars() { return [] } - - /** - * @param {string} name - */ - hasModifier(name) { - return ((this.minified.m | 0) & getModifierBit(name)) !== 0; - } - - toSource() { - return this.name; - } -} - -class MTIArrayType extends MTITypeBase { - /** - * @param {Type} element_type - */ - constructor(element_type) { - super({ - n: element_type.name + '[]', - d: '', - m: 0, // should array types be implicitly final? - }); - this.element_type = element_type; - } - - get fullyDottedRawName() { return `${this.element_type.fullyDottedRawName}[]` } - - /** @type {MTITypeKind} */ - get typeKind() { return 'array' } -} - -class MTIPrimitiveType extends MTITypeBase { - - static _cached = new Map(); - static fromName(name) { - let value = MTIPrimitiveType._cached.get(name); - if (!value) { - value = new MTIPrimitiveType({ - n: name, - d: '', - m: 0, - }); - MTIPrimitiveType._cached.set(name, value); - } - return value; - } - - get fullyDottedRawName() { return this.name } - - /** @type {MTITypeKind} */ - get typeKind() { return 'primitive' } -} - -/** - * MTIType encodes a complete type (class, interface or enum) - * ``` - * { - * d: string - type docs - * p: pkgref - the package this type belongs to - * n: string - type name (in x$y format for enclosed types) - * v: typeref[] - generic type variables - * e: typeref | typeref[] - super/extends type (single value for classes, array for interfaces) - * i: typeref[] - interface types - * f: mtifield[] - fields - * c: mtictrs[] - constructors - * g: mtimethod[] - methods - * } - * ``` - */ -class MTIType extends MTITypeBase { - - /** - * @param {MTI} unit - * @param {*} mti - */ - constructor(unit, mti) { - super(mti); - this.parsed = { - package: packageNameFromRef(mti.p, unit), - - /** @type {ReferencedType[]} */ - typevars: mti.v.map(v => typeFromRef(v, unit)), - - /** @type {ReferencedType|ReferencedType[]} */ - extends: Array.isArray(mti.e) - ? mti.e.map(e => typeFromRef(e, unit)) - : typeFromRef(mti.e, unit), - - /** @type {ReferencedType[]} */ - implements: mti.i.map(i => typeFromRef(i, unit)), - - /** @type {MTIField[]} */ - fields: mti.f.map(f => new MTIField(unit, f)), - - /** @type {MTIConstructor[]} */ - constructors: mti.c.map(c => new MTIConstructor(unit, c)), - - /** - * MTI method are grouped by name - we split them here - * @type {MTIMethod[]} - */ - methods: mti.g.reduce((arr, m) => [...arr, ...MTIMethod.split(unit, this, m)], []), - } - } - - get dottedRawName() { return this.minified.n.replace(/[$]/g, '.') }; - - get fullyDottedRawName() { - const pkg = this.package; - return pkg ? `${pkg}.${this.dottedRawName}` : this.dottedRawName; - }; - - get dottedName() { - const t = this.typevars.map(t => t.name).join(','); - return t ? `${this.dottedRawName}<${t}>` : this.dottedRawName; - }; - - /** - * type name with no qualifiers - * @type {string} - */ - get simpleRawName() { return this.minified.n.match(/[^$]+$/)[0] } - - /** - * package this type belongs to - */ - get package() { return this.parsed.package } - - /** @type {MTITypeKind} */ - get typeKind() { - const m = this.minified.m; - return (m & TypeModifiers.enum) - ? 'enum' : (m & TypeModifiers.interface) - ? 'interface' : (m & TypeModifiers['@interface']) - ? '@interface' : 'class'; - } - - /** - * generic type variables - */ - get typevars() { return this.parsed.typevars } - - /** - * class or interface extends. - * Note that classes have a single extend type, but interfaces have an array. - */ - get extends() { return this.parsed.extends } - - /** - * class implements - */ - get implements() { return this.parsed.implements } - - /** - * @type {MTIConstructor[]} - */ - get constructors() { return this.parsed.constructors } - - /** - * @type {MTIField[]} - */ - get fields() { return this.parsed.fields } - - /** - * @type {MTIMethod[]} - */ - get methods() { return this.parsed.methods } - - toSource() { - let constructors = [], typevars = '', ex = '', imp = ''; - - // only add constructors if there's more than just the default constructor - if (!((this.constructors.length === 1) && (this.constructors[0].parameters.length === 0))) { - constructors = this.constructors; - } - - if (this.typevars.length) { - typevars = `<${this.typevars.map(tv => tv.name).join(',')}>`; - } - - if (this.extends) { - // only add extends if it's not derived from java.lang.Object - if (this.extends !== KnownTypes[3]) { - const x = Array.isArray(this.extends) ? this.extends : [this.extends]; - if (x.length) { - ex = `extends ${x.map(type => type.dottedName).join(', ')} `; - } - } - } - - if (this.implements.length) { - imp = `implements ${this.implements.map(type => type.dottedName).join(', ')} `; - } - - return [ - `${this.fmtdocs()}${typemods(this.modifiers)} ${this.simpleRawName}${typevars} ${ex}${imp}{`, - ...this.fields.map(f => indent(f.toSource())), - ...constructors.map(c => indent(c.toSource())), - ...this.methods.map(m => indent(m.toSource())), - `}` - ].join('\n'); - } - - /** - * @param {MTI} unit - * @param {number} typeref - */ - setExtends(unit, typeref) { - if (Array.isArray(this.minified.e)) { - this.minified.e.push(typeref); - // @ts-ignore - this.parsed.extends.push(typeFromRef(typeref, unit)); - } else { - this.minified.e = typeref; - this.parsed.extends = typeFromRef(typeref, unit); - } - } - - /** - * @param {MTI} unit - * @param {string} docs - * @param {string[]} modifiers - * @param {number} typeref - * @param {string} name - */ - addField(unit, docs, modifiers, typeref, name) { - const o = { - d: docs, - m: getAccessMods(modifiers), - n: name, - t: typeref, - } - this.minified.f.push(o); - this.parsed.fields.push(new MTIField(unit, o)); - } - - /** - * @param {MTI} unit - * @param {string} docs - * @param {string[]} modifiers - */ - addConstructor(unit, docs, modifiers) { - const o = { - d: docs, - m: getAccessMods(modifiers), - p: [], - } - this.minified.c.push(o); - const c = new MTIConstructor(unit, o); - this.parsed.constructors.push(c); - return c; - } - - /** - * @param {MTI} unit - * @param {MTIType} owner - * @param {string} docs - * @param {string[]} modifiers - * @param {number} typeref - * @param {string} name - */ - addMethod(unit, owner, docs, modifiers, typeref, name) { - let g = this.minified.g.find(m => m.name === name); - if (!g) { - g = { - n:name, - s: [], - } - this.minified.g.push(g); - } - const o = { - d: docs, - m: getAccessMods(modifiers), - t: typeref, - p: [], - }; - g.s.push(o); - const method = new MTIMethod(unit, owner, name, o); - this.parsed.methods.push(method); - return method; - } -} - -/** - * MTIField encodes a single type field. - * ``` - * { - * d: string - docs - * m: number - access modifiers - * n: string - field name - * t: typeref - field type - * } - * ``` - */ -class MTIField extends MinifiableInfo { - - /** - * @param {MTI} owner - * @param {*} mti - */ - constructor(owner, mti) { - super(mti); - this.parsed = { - type: typeFromRef(mti.t, owner), - }; - } - - /** - * @type {number} - */ - get modifiers() { return this.minified.m } - - /** - * @type {string} - */ - get docs() { return this.minified.d } - - /** - * @type {string} - */ - get name() { return this.minified.n } - - /** - * @type {ReferencedType} - */ - get type() { return this.parsed.type } - - toSource() { - return `${this.fmtdocs()}${access(this.modifiers)}${this.type.dottedName} ${this.name} = ${this.type.defaultValue};` - } -} - -class MTIMethodBase extends MinifiableInfo {} - -/** - * MTIContructor encodes a single type constructor. - * ``` - * { - * d: string - docs - * m: number - access modifiers - * p: mtiparam[] - constructor parameters - * } - * ``` - */ -class MTIConstructor extends MTIMethodBase { - - /** - * @param {MTI} owner - * @param {*} mti - */ - constructor(owner, mti) { - super(mti); - this.parsed = { - typename: owner.minified.it[0].n, - /** @type {MTIParameter[]} */ - parameters: mti.p.map(p => new MTIParameter(owner, p)), - } - } - - /** - * @type {number} - */ - get modifiers() { return this.minified.m } - - get docs() { return this.minified.d } - - /** - * @type {MTIParameter[]} - */ - get parameters() { return this.parsed.parameters } - - toSource() { - const typename = this.parsed.typename.split('$').pop(); - return `${this.fmtdocs()}${access(this.modifiers)}${typename}(${this.parameters.map(p => p.toSource()).join(', ')}) {}` - } - - /** - * @param {MTI} unit - * @param {string[]} modifiers - * @param {number} typeref - * @param {string} name - */ - addParameter(unit, modifiers, typeref, name) { - const o = { - m: getAccessMods(modifiers), - t: typeref, - n: name, - } - this.minified.p.push(o); - this.parsed.parameters.push(new MTIParameter(unit, o)); - } -} - -/** - * MTIMethod encodes a single type method. - * - * In minified form, methods are encoded as overloads - each entry - * has a single name with one or more method signatures. - * ``` - * { - * d: string - docs - * n: string - method name - * s: [{ - * m: number - access modifiers - * t: typeref - return type - * p: mtiparam[] - method parameters - * }, - * ... - * ] - * - * } - * ``` - */ - class MTIMethod extends MTIMethodBase { - - /** - * @param {MTI} unit - * @param {MTIType} type - * @param {string} name - * @param {*} mti - */ - constructor(unit, type, name, mti) { - super(mti); - this.interfaceMethod = type.modifiers & 0x200; - this.parsed = { - name, - /** @type {MTIParameter[]} */ - parameters: mti.p.map(p => new MTIParameter(unit, p)), - /** @type {ReferencedType} */ - return_type: typeFromRef(mti.t, unit), - } - } - - /** - * @param {MTI} unit - * @param {MTIType} type - * @param {*} mti - */ - static split(unit, type, mti) { - return mti.s.map(s => new MTIMethod(unit, type, mti.n, s)); - } - - /** - * @type {string} - */ - get docs() { return this.minified.d } - - /** - * @type {number} - */ - get modifiers() { return this.minified.m } - - /** - * @type {ReferencedType} - */ - get return_type() { return this.parsed.return_type } - - /** - * @type {string} - */ - get name() { return this.parsed.name } - - /** - * @type {MTIParameter[]} - */ - get parameters() { return this.parsed.parameters } - - toDeclSource() { - return `${this.return_type.dottedName} ${this.name}(${this.parameters.map(p => p.toSource()).join(', ')})`; - } - - toSource() { - let m = this.modifiers, body = ' {}'; - if (m & 0x400) { - body = ';'; // abstract method - no body - } else if (this.return_type.name !== 'void') { - body = ` { return ${this.return_type.defaultValue}; }`; - } - if (this.interfaceMethod) { - m &= ~0x400; // exclude abstract modifier as it's redundant - } - return `${this.fmtdocs()}${access(m)}${this.return_type.dottedName} ${this.name}(${this.parameters.map(p => p.toSource()).join(', ')})${body}` - } - - /** - * @param {MTI} unit - * @param {string[]} modifiers - * @param {number} typeref - * @param {string} name - */ - addParameter(unit, modifiers, typeref, name) { - const o = { - m: getAccessMods(modifiers), - t: typeref, - n: name, - } - this.minified.p.push(o); - this.parsed.parameters.push(new MTIParameter(unit, o)); - } -} - -/** - * MTIParameter encodes a single method or constructor paramter - * ``` - * { - * m?: number - access modifiers (only 'final' is allowed) - * t: typeref - parameter type - * n: string - parameter name - * } - * ``` - */ -class MTIParameter extends MinifiableInfo { - - /** - * @param {MTI} owner - * @param {*} mti - */ - constructor(owner, mti) { - super(mti); - this.parsed = { - type: typeFromRef(mti.t, owner) - } - } - - /** - * @type {number} - */ - get modifiers() { return this.minified.m | 0 } - - /** - * @type {string} - */ - get name() { return this.minified.n } - - /** - * @type {ReferencedType} - */ - get type() { return this.parsed.type } - - toSource() { - return `${access(this.modifiers)}${this.type.dottedName} ${this.name}` - } -} - -const access_keywords = 'public private protected static final synchronized volatile transient native interface abstract strict'.split(' '); - -/** - * @param {number} modifier_bits - */ -function access(modifier_bits) { - // convert the modifier bits into keywords - const decls = access_keywords.filter((_,i) => modifier_bits & (1 << i)); - if (decls.length) { - decls.push(''); // make sure we end with a space - } - return decls.join(' '); -} - -/** - * @param {string} modifier - */ -function getModifierBit(modifier) { - const i = access_keywords.indexOf(modifier); - return i < 0 ? 0 : (1 << i); -} - -/** - * @param {string[]} modifiers - * @param {boolean} [varargs] - */ -function getAccessMods(modifiers, varargs = false) { - let m = 0; - modifiers.forEach(modifier => m |= getModifierBit(modifier)); - if (varargs) { - m |= getModifierBit('transient'); - } - return m; -} - -const TypeModifiers = { - public: 0b0000_0000_0000_0001, // 0x1 - final: 0b0000_0000_0001_0000, // 0x10 - interface: 0b0000_0010_0000_0000, // 0x200 - abstract: 0b0000_0100_0000_0000, // 0x400 - '@interface': 0b0010_0000_0000_0000, // 0x2000 - enum: 0b0100_0000_0000_0000, // 0x4000 -} - -/** - * @param {number} modifier_bits - */ -function typemods(modifier_bits) { - const modifiers = []; - let type = 'class'; - if (modifier_bits & TypeModifiers.interface) { - type = 'interface'; - modifier_bits &= ~TypeModifiers.abstract; // ignore abstract keyword for interfaces - } else if (modifier_bits & TypeModifiers['@interface']) { - type = '@interface'; - } else if (modifier_bits & TypeModifiers.enum) { - type = 'enum'; - modifier_bits &= ~TypeModifiers.final; // ignore final keyword for enums - } - if (modifier_bits & TypeModifiers.public) modifiers.push('public'); - if (modifier_bits & TypeModifiers.final) modifiers.push('final'); - if (modifier_bits & TypeModifiers.abstract) modifiers.push('abstract'); - modifiers.push(type); - return modifiers.join(' '); -} - -/** - * @param {string[]} modifiers - * @param {MTITypeKind} typeKind - */ -function getTypeMods(modifiers, typeKind) { - let m = 0; - if (modifiers.includes('public')) m |= TypeModifiers.public; - if (modifiers.includes('final')) m |= TypeModifiers.final; - if (modifiers.includes('abstract')) m |= TypeModifiers.abstract; - switch (typeKind) { - case "interface": - m |= TypeModifiers.interface | TypeModifiers.abstract; - break; - case "@interface": - m |= TypeModifiers['@interface'] | TypeModifiers.abstract; - break; - case "enum": - m |= TypeModifiers.enum | TypeModifiers.final; - break; - } - return m; -} - -/** - * List of known/common packages. - * These are used/encoded as pkgrefs between 0 and 15. - */ -const KnownPackages = ["java.lang","java.io","java.util",""]; - -/** - * Literals corresponding to the KnownTypes. - * These are used for method return values and field expressions when constructing source. - */ -const KnownTypeValues = ['','0','""','null','false',"'\\0'",'0','0l','0','0.0f','0.0d','null']; - -/** - * List of known/common types. - * These are used/encoded as typerefs between 0 and 15. - */ -const KnownTypes = [ - "void","int","String","Object","boolean","char","byte","long","short","float","double","Class" -].map((n,i) => { - const pkg_or_prim = /^[SOC]/.test(n) ? KnownPackages[0] : false; - return new ReferencedType(null, {n}, pkg_or_prim, KnownTypeValues[i]); -}); - -module.exports = MTI; diff --git a/langserver/java/parser9.js b/langserver/java/parser9.js index 5293129..f5baac5 100644 --- a/langserver/java/parser9.js +++ b/langserver/java/parser9.js @@ -329,6 +329,7 @@ const markers = { annotation: 'Q', brackets: 'R', typeArgs: 'T', + enumvalues: 'U', varDecl: 'V', typeDecl: 'Z', error: ' ', @@ -342,9 +343,10 @@ const markers = { * @param {string} [marker] * @param {boolean} [recursive] * @param {{}} [parseClass] + * @param {{time:boolean}} [opts] */ -function group(sourceblocks, id, re, marker, recursive, parseClass) { - console.time(id); +function group(sourceblocks, id, re, marker, recursive, parseClass, opts) { + if (opts && opts.time) console.time(id); let grouped = []; let sourcemap = sourceblocks.sourcemap(); if (!re.global) { @@ -356,7 +358,9 @@ function group(sourceblocks, id, re, marker, recursive, parseClass) { for (let m; m = re.exec(sourcemap.simplified); ) { // every group must start and end on a definite boundary const start = sourcemap.map[m.index]; - const end = sourcemap.map[m.index + m[0].length -1]; + let end = sourcemap.map[m.index + m[0].length - 1]; + if (end === undefined) + end = sourcemap.map[m.index + m[0].length]; if (start === undefined || end === undefined) { throw new Error('undefined group boundary') } @@ -380,7 +384,7 @@ function group(sourceblocks, id, re, marker, recursive, parseClass) { } break; } - console.timeEnd(id); + if (opts && opts.time) console.timeEnd(id); return grouped; } @@ -439,6 +443,20 @@ class DeclaredVariableBlock extends DeclarationBlock { class FieldBlock extends DeclaredVariableBlock { } +class EnumValueBlock extends TextBlock { + + static parseRE = /(?<=^\{\s*)[W](\s*=[^,;]*)?(\s*,\s*[W](\s*=[^,;]*)?)*(\s*;)?/g + + /** + * @param {TextBlockArray} section + * @param {string} simplified + */ + constructor(section, simplified) { + super(section, simplified); + } + +} + class ParameterBlock extends DeclaredVariableBlock { static parseRE = /([MQ](\s*[MQ])*\s+)?(V)/g } @@ -476,19 +494,19 @@ class MCBlock extends DeclarationBlock { this.parsed.parameters = group(param_block, 'param', ParameterBlock.parseRE, markers.parameter, false, ParameterBlock); // parameters must be a comma-separated list const sm = param_block.sourcemap(); - if (sm.simplified.search(/^\(( *F( *, *F)*)? *\)/) === 0) { - return; + if (sm.simplified.search(/^\((\s*F(\s*,\s*F)*)?\s*\)/) === 0) { + return this.parsed.parameters; } - let invalid = sm.simplified.match(/^(\( *)(F?)(?: *, *F)* */); + let invalid = sm.simplified.match(/^(\(\s*)(F?)(?:\s*,\s*F)*\s*/); if (!invalid) { // should never happen, but ignore - return; + return this.parsed.parameters; } const token_idx = invalid[2] ? sm.map[invalid[0].length] // there's a problem with a subsequent declaration : sm.map[invalid[1].length] // there's a problem with the first declaration const token = param_block.blocks[token_idx]; - if (!token) return; + if (!token) return this.parsed.parameters; this.parsed.errors = [token]; } return this.parsed.parameters; @@ -594,17 +612,21 @@ class TypeDeclBlock extends DeclarationBlock { static marker = 'Z'; /** - * * @param {TextBlockArray} blocks * @param {string} simplified */ constructor(blocks, simplified) { super(blocks, simplified); this.decl = blocks; + this.kindToken = this.decl.blocks.find(b => !/^[MQ\s]/.test(b.simplified)); this.name_token = this.decl.blocks.find(b => b.simplified.startsWith('W')); this.typevars_token = this.decl.blocks.find(b => b.simplified.startsWith('T')); - this.extends_token = this.decl.blocks.find(b => b.simplified.startsWith('J')); - this.implements_token = this.decl.blocks.find(b => b.simplified.startsWith('K')); + this.extends_decl = this.decl.blocks.find(b => b.simplified.startsWith('J')); + this.implements_decl = this.decl.blocks.find(b => b.simplified.startsWith('K')); + /** @type {TypeDeclBlock} */ + this.outer_type = null; + /** @type {ModuleBlock} */ + this.mod = null; this.parsed = { /** @type {{name: string, decl:(TextBlock|BoundedTypeVar)}[]} */ typevars: null, @@ -627,13 +649,24 @@ class TypeDeclBlock extends DeclarationBlock { * Return the kind of type declared */ kind() { - const kindToken = this.decl.blocks.find(b => !/^[MQ\s]/.test(b.simplified)); /** @type {'class'|'enum'|'interface'|'@'} */ // @ts-ignore - const id = kindToken.toSource(); + const id = this.kindToken.toSource(); return id === '@' ? '@interface' : id; } + get fullyDottedName() { + return this.shortSignature.replace(/[/$]/g, '.'); + } + + get shortSignature() { + if (this.outer_type) { + return `${this.outer_type.shortSignature}$${this.simpleName}` + } + const pkg = this.mod.packageName.replace(/\./g, '/'); + return `${pkg}${pkg ? '/' : ''}${this.simpleName}` + } + /** * Return the type name with no type-parameter info */ @@ -714,9 +747,13 @@ class TypeDeclBlock extends DeclarationBlock { parseAnnotations(body); parseEITDecls(body); /** @type {TypeDeclBlock[]} */ - this.parsed.types = parseTypeDecls(body); + this.parsed.types = parseTypeDecls(body, this, this.mod); group(body, 'var-decl', VarDeclBlock.parseRE, markers.varDecl, false, VarDeclBlock); + if (this.kind() === 'enum') { + /** @type {EnumValueBlock[]} */ + this.parsed.enums = group(body, 'enumvalue', EnumValueBlock.parseRE, markers.enumvalues, false, EnumValueBlock); + } /** @type {FieldBlock[]} */ this.parsed.fields = group(body, 'field', FieldBlock.parseRE, markers.field, false, FieldBlock); /** @type {MethodBlock[]} */ @@ -725,9 +762,9 @@ class TypeDeclBlock extends DeclarationBlock { this.parsed.constructors = group(body, 'constructor', ConstructorBlock.parseRE, markers.constructor, false, ConstructorBlock); /** @type {InitialiserBlock[]} */ this.parsed.initialisers = group(body, 'initialiser', InitialiserBlock.parseRE, markers.initialiser, false, InitialiserBlock); - // anything other than types, fields, methods, constructors and initialisers are errors + // anything other than types, fields, methods, constructors, enums and initialisers are errors /** @type {TextBlock[]} */ - this.parsed.errors = group(body, 'type-body-error', /[^{}ZFGCE\s]/g, markers.error); + this.parsed.errors = group(body, 'type-body-error', /[^{}ZFGCEU\s;]+/g, markers.error); } } @@ -838,6 +875,10 @@ class ModuleBlock extends TextBlockArray { return this._ensureParsed().types; } + get parseErrors() { + return this._ensureParsed().errors; + } + _ensureParsed() { if (this._parsed) { return this._parsed; @@ -848,11 +889,14 @@ class ModuleBlock extends TextBlockArray { parseTypeArgs(this); parseAnnotations(this); parseEITDecls(this); - const types = parseTypeDecls(this); + const types = parseTypeDecls(this, null, this); + // anything that's not a package, import or type declaration is an error + const errors = group(this, 'module-errors', /[^NOZ;\s]+/g, ' '); return this._parsed = { packages, imports, types, + errors, } } } @@ -912,10 +956,17 @@ function parseEITDecls(sourceblocks) { /** * @param {TextBlockArray} sourceblocks - * @return {TypeDeclBlock[]} + * @param {TypeDeclBlock} outer_type + * @param {ModuleBlock} mod */ -function parseTypeDecls(sourceblocks) { - return group(sourceblocks, 'type-decl', TypeDeclBlock.parseRE, markers.typeDecl, false, TypeDeclBlock); +function parseTypeDecls(sourceblocks, outer_type, mod) { + /** @type {TypeDeclBlock[]} */ + const typedecls = group(sourceblocks, 'type-decl', TypeDeclBlock.parseRE, markers.typeDecl, false, TypeDeclBlock); + typedecls.forEach(td => { + td.outer_type = outer_type; + td.mod = mod; + }); + return typedecls; } /** diff --git a/langserver/java/parsetypes/import.js b/langserver/java/parsetypes/import.js index eb301a9..6e3d313 100644 --- a/langserver/java/parsetypes/import.js +++ b/langserver/java/parsetypes/import.js @@ -1,7 +1,5 @@ const Declaration = require('./declaration'); -const ParseProblem = require('./parse-problem'); const Token = require('./token'); -const TypeParameters = require('./type-parameters'); /** * @typedef {import('./modifier')} Modifier @@ -34,35 +32,6 @@ class ImportDeclaration extends Declaration { lastToken() { return this.semicolon || this.asterisk || this.nameparts.slice(-1)[0]; } - - validate() { - const checkModifierIsStatic = () => { - if (this.static_ && this.static_.text !== 'static') { - return ParseProblem.syntaxError(this.static_); - } - } - - const checkNoInvalidModifiers = () => { - return this.modifiers.map(modifier => { - if (modifier instanceof Token) { - return ParseProblem.syntaxError(modifier); - } - if (modifier instanceof TypeParameters) { - return ParseProblem.syntaxError(modifier.open); - } - }) - } - - /** @type {ParseProblem[]} */ - const problems = [ - checkModifierIsStatic(), - ...ParseProblem.checkNonKeywordIdents(this.nameparts), - ParseProblem.checkSemicolon(this), - ...checkNoInvalidModifiers(), - ]; - - return problems; - } } module.exports = ImportDeclaration; diff --git a/langserver/java/parsetypes/resolved-import.js b/langserver/java/parsetypes/resolved-import.js index 5f9acf0..df41f08 100644 --- a/langserver/java/parsetypes/resolved-import.js +++ b/langserver/java/parsetypes/resolved-import.js @@ -1,17 +1,20 @@ +/** + * @typedef {import('java-mti').JavaType} JavaType + */ const { ImportBlock } = require('../parser9'); /** * 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 an MTI which lists the implementation details of the type (fields, methods, etc). + * Each type is mapped to a JavaType which lists the implementation details of the type (fields, methods, etc). * */ class ResolvedImport { /** * @param {ImportBlock} import_decl * @param {RegExpMatchArray} matches - * @param {Map} typemap + * @param {Map} typemap * @param {'owner-package'|'import'|'implicit-import'} import_kind */ constructor(import_decl, matches, typemap, import_kind) { @@ -27,7 +30,7 @@ const { ImportBlock } = require('../parser9'); this.fullyQualifiedNames = Array.from(matches); /** - * THe map of fully-qualified type names to MTIs + * THe map of fully-qualified type names to JavaTypes */ this.types = new Map(matches.map(name => [name, typemap.get(name)])); diff --git a/langserver/java/parsetypes/resolved-type.js b/langserver/java/parsetypes/resolved-type.js index e266367..12ecbb1 100644 --- a/langserver/java/parsetypes/resolved-type.js +++ b/langserver/java/parsetypes/resolved-type.js @@ -1,6 +1,7 @@ /** * @typedef {import('./token')} Token * @typedef {import('./type')} TypeDeclaration + * @typedef {import('java-mti').JavaType} JavaType */ /** @@ -78,8 +79,8 @@ class ResolvedType { error = ''; /** - * The resolved MTIs that match this type. This will be an empty array if the type cannot be found. - * @type {import('../mti').Type[]} + * The resolved JavaTypes that match this type. This will be an empty array if the type cannot be found. + * @type {JavaType[]} */ mtis = []; diff --git a/langserver/java/source-type.js b/langserver/java/source-type.js new file mode 100644 index 0000000..d44d04c --- /dev/null +++ b/langserver/java/source-type.js @@ -0,0 +1,182 @@ +const { JavaType, CEIType, Method, Field, Parameter, TypeVariable, UnresolvedType } = require('java-mti'); +const { ModuleBlock, TypeDeclBlock, FieldBlock, MethodBlock, ParameterBlock, TextBlock } = require('./parser9'); + +/** + * + * @param {{modifiers:TextBlock[]}} x + */ +function mapmods(x) { + return x.modifiers.map(m => m.source); +} + +/** + * + * @param {TextBlock} decl + */ +function extractTypeList(decl) { + if (!decl) { + return []; + } + const types = []; + const re = /[WD]( *[WDT.])*/g; + const declba = decl.blockArray(); + const sm = declba.sourcemap(); + for (let m; m = re.exec(sm.simplified);) { + const start = sm.map[m.index], end = sm.map[m.index + m[0].length-1]; + const block_range = declba.blocks.slice(start, end+1); + types.push(block_range); + } + return types.map(tokens => { + const decl = { + type: tokens.map(t => t.source).join(''), + typeTokens: tokens, + } + return new ResolvableType(decl); + }); +} + +class SourceType extends CEIType { + /** + * @param {ModuleBlock} mod + * @param {TypeDeclBlock} type + * @param {string} qualified_type_name qualified $-separated type name + */ + constructor(mod, type, qualified_type_name) { + super(type.shortSignature, type.kind(), mapmods(type), type.docs); + this._decl = type; + super.packageName = mod.packageName; + super.simpleTypeName = type.simpleName; + super.dottedTypeName = qualified_type_name.replace(/\$/g, '.'); + super.fullyDottedRawName = type.fullyDottedName; + + this.extends_types = type.extends_decl ? extractTypeList(type.extends_decl) : [] + this.implements_types = type.implements_decl ? extractTypeList(type.implements_decl) : []; + this.implicit_extend = !this.extends_types.length && !this.implements_types.length ? [new ResolvableType({type: 'java.lang.Object', typeTokens:[]})] : []; + + this.fields = type.fields.map(f => new SourceField(this, f)); + this.methods = type.methods.map(m => new SourceMethod(this, m)); + super.typevars = type.typevars.map(tv => { + const typevar = new TypeVariable(tv.name); + // automatically add the Object bound + typevar.bounds.push(new TypeVariable.Bound(this, 'Ljava/lang/Object;', false)); + return typevar; + }); + } + + get supers() { + return [ + ...this.implicit_extend.map(t => t.resolved), + ...this.extends_types.map(t => t.resolved), + ...this.implements_types.map(t => t.resolved) + ]; + } + + getAllResolvableTypes() { + /** @type {ResolvableType[]} */ + const res = [ + ...this.implicit_extend, + ...this.extends_types, + ...this.implements_types, + ]; + this.fields.forEach(f => res.push(f._type)); + this.methods.forEach(m => { + res.push(m._returnType); + m.parameters.forEach(p => res.push(p._paramType)); + }); + return res; + } +} + +class SourceField extends Field { + /** + * @param {SourceType} owner + * @param {FieldBlock} decl + */ + constructor(owner, decl) { + super(mapmods(decl), decl.docs); + this._owner = owner; + this._decl = decl; + this._type = new ResolvableType(decl); + } + + get name() { + return this._decl.name; + } + + get type() { + return this._type.resolved; + } +} + +class SourceMethod extends Method { + /** + * @param {SourceType} owner + * @param {MethodBlock} decl + */ + constructor(owner, decl) { + super(mapmods(decl), decl.docs); + this._owner = owner; + this._decl = decl; + this._parameters = decl.parameters.map((p,i) => new SourceParameter(p)); + this._returnType = new ResolvableType(decl); + } + + get name() { + return this._decl.name; + } + + /** + * @returns {SourceParameter[]} + */ + get parameters() { + return this._parameters; + } + + get returnType() { + return this._returnType.resolved; + } +} + +class SourceParameter extends Parameter { + /** + * @param {ParameterBlock} decl + * @param {ResolvableType} [type] + */ + constructor(decl, type = new ResolvableType(decl)) { + super(decl.name, type, decl.isVarArgs); + this._decl = decl; + this._paramType = type; + } + + get type() { + return this._paramType.resolved; + } +} + +class ResolvableType extends UnresolvedType { + /** + * + * @param {{type:string, typeTokens:TextBlock[]}} decl + */ + constructor(decl) { + super(decl.type); + this._decl = decl; + /** @type {import('../java/parsetypes/resolved-type')} */ + this._resolved = null; + } + + /** + * @returns {JavaType} + */ + get resolved() { + if (!this._resolved) return this; + if (!this._resolved.mtis.length) return this; + return this._resolved.mtis[0]; + } + + get typeTokens() { + return this._decl.typeTokens; + } +} + +exports.SourceType = SourceType; diff --git a/langserver/java/type-resolver.js b/langserver/java/type-resolver.js index ed4a748..923d7b3 100644 --- a/langserver/java/type-resolver.js +++ b/langserver/java/type-resolver.js @@ -1,5 +1,8 @@ +/** + * @typedef {Map} TypeMap + */ +const { JavaType, PrimitiveType, ArrayType, CEIType } = require('java-mti'); const { ResolvedImport } = require('./import-resolver'); -const MTI = require('./mti'); const ResolvedType = require('./parsetypes/resolved-type'); /** @@ -85,9 +88,6 @@ function createTypeScopeRegex(fully_qualified_scope, dotted_raw_typename) { // split the type name across enclosed type boundaries const scopes = fully_qualified_scope.split('$'); - // the first scope is the dotted package name and top-level type - we need to escape the package-qualifier dots for regex - scopes[0] = scopes[0].replace(/\./g,'[.]'); - // 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,'[$]'); @@ -102,21 +102,21 @@ function createTypeScopeRegex(fully_qualified_scope, dotted_raw_typename) { } /** - * Locate MTIs that match a type. - * @param {string} typename The type to resolve + * 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 {Map} typemap the global list of types + * @param {TypeMap} typemap */ -function resolveType(typename, fully_qualified_scope, resolved_imports, typemap) { - const { type, error } = parse_type(typename); +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 MTIs for the type and type arguments + // locate the JavaTypes for the type and type arguments resolveCompleteType(type, fully_qualified_scope, resolved_imports, typemap); return type; } @@ -126,11 +126,11 @@ function resolveType(typename, fully_qualified_scope, resolved_imports, typemap) * @param {ResolvedType} type * @param {string} fully_qualified_scope * @param {ResolvedImport[]} resolved_imports - * @param {Map} typemap + * @param {TypeMap} typemap */ function resolveCompleteType(type, fully_qualified_scope, resolved_imports, typemap) { - type.mtis = findTypeMTIs(type.getDottedRawType(), type.arrdims, 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 => { @@ -146,34 +146,34 @@ function resolveCompleteType(type, fully_qualified_scope, resolved_imports, type * @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 {Map} typemap + * @param {TypeMap} typemap */ -function findTypeMTIs(dotted_raw_typename, arraydims, fully_qualified_scope, resolved_imports, typemap) { - let mtis = findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_imports, 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 MTIs - mtis.forEach((mti,idx,arr) => { - arr[idx] = MTI.makeArrayType(mti, arraydims); - }) + // convert matches to array types + const array_types = types.map(t => new ArrayType(t, arraydims)); + return array_types; } - return mtis; + return types; } /** - * Match a dotted type name to one or more MTIs + * 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 {Map} typemap + * @param {TypeMap} typemap * @param {ResolvedImport[]} resolved_imports The list of types resolved from the imports + * @returns {(PrimitiveType|CEIType)[]} */ -function findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_imports, typemap) { +function findRawJavaTypes(dotted_raw_typename, fully_qualified_scope, resolved_imports, typemap) { // first check if it's a simple primitive - if (/^(int|char|boolean|void|long|byte|short|float|double)$/.test(dotted_raw_typename)) { + if (PrimitiveType.isPrimitiveTypeName(dotted_raw_typename)) { // return the primitive type - return [MTI.fromPrimitive(dotted_raw_typename)]; + return [PrimitiveType.fromName(dotted_raw_typename)]; } // create a regex to search for the type name @@ -190,7 +190,7 @@ function findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_im // 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 = new RegExp(`^(.+?/)?${dotted_raw_typename.replace(/\./g,'[/$]')}$`); // search the imports for the type matched_types = @@ -211,7 +211,7 @@ function findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_im 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,'[.$]')}$`); + search = new RegExp(`^${dotted_raw_typename.replace(/\./g,'[/$]')}$`); for (let typename of typemap.keys()) { if (search.test(typename)) { matched_types = [{ @@ -227,7 +227,8 @@ function findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_im // - 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((mtis,mt) => [...mtis, ...mt.mtis] , []); + return matched_types + .reduce((types, type) => [...types, ...type.mtis] , []) } /** @@ -235,7 +236,7 @@ function findRawTypeMTIs(dotted_raw_typename, fully_qualified_scope, resolved_im * @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 {Map} typemap + * @param {TypeMap} typemap */ function resolveTypes(types, fully_qualified_scope, resolved_imports, typemap) { return types.map(typename => resolveType(typename, fully_qualified_scope, resolved_imports, typemap)); @@ -246,7 +247,7 @@ function resolveTypes(types, fully_qualified_scope, resolved_imports, typemap) { * @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 {Map} typemap + * @param {TypeMap} typemap */ function resolveTypeIdents(types, fully_qualified_scope, resolved_imports, typemap) { const names = types.map(typeident => @@ -258,6 +259,7 @@ function resolveTypeIdents(types, fully_qualified_scope, resolved_imports, typem module.exports = { parse_type, + resolveType, resolveTypes, resolveTypeIdents, ResolvedType, diff --git a/langserver/java/validater.js b/langserver/java/validater.js index bdd766f..445ff3e 100644 --- a/langserver/java/validater.js +++ b/langserver/java/validater.js @@ -1,36 +1,59 @@ const { ModuleBlock, TypeDeclBlock } = require('./parser9'); const { resolveImports } = require('../java/import-resolver'); -const MTI = require('./mti'); +const ResolvedImport = require('../java/parsetypes/resolved-import'); +const { resolveType } = require('../java/type-resolver'); +const { SourceType } = require('./source-type'); +const { JavaType } = require('java-mti'); /** - * @param {string} package_name + * @param {ModuleBlock} mod * @param {string} owner_typename * @param {ModuleBlock|TypeDeclBlock} parent - * @param {MTI.Type[]} mtis + * @param {SourceType[]} source_types */ -function getSourceMTIs(package_name, owner_typename, parent, mtis) { +function getSourceTypes(mod, owner_typename, parent, source_types) { parent.types.forEach(type => { - const mods = type.modifiers.map(m => m.source); const qualifiedTypeName = `${owner_typename}${type.simpleName}`; // we add the names of type variables here, but we resolve any bounds later - const typevar_names = type.typevars.map(tv => tv.name); - const mti = new MTI().addType(package_name, '', mods, type.kind(), qualifiedTypeName, typevar_names); - mtis.push(mti); - getSourceMTIs(package_name, `${qualifiedTypeName}$`, type, mtis); + //const typevar_names = type.typevars.map(tv => tv.name); + //const mti = new MTI().addType(package_name, '', mods, type.kind(), qualifiedTypeName, typevar_names); + const t = new SourceType(mod, type, qualifiedTypeName); + source_types.push(t); + getSourceTypes(mod, `${qualifiedTypeName}$`, type, source_types); }); } +/** + * + * @param {SourceType} source_type + * @param {ResolvedImport[]} resolved_imports + * @param {Map} typemap + */ +function resolveResolvableTypes(source_type, resolved_imports, typemap) { + const fully_qualified_scope = source_type.shortSignature; + const resolvableTypes = source_type.getAllResolvableTypes(); + resolvableTypes.forEach(rt => { + rt._resolved = resolveType(rt.label, fully_qualified_scope, resolved_imports, typemap); + }) +} + /** * @param {ModuleBlock} mod + * @param {Map} androidLibrary */ function validate(mod, androidLibrary) { console.time('validation'); - const source_mtis = []; - getSourceMTIs(mod.packageName, '', mod, source_mtis); + /** @type {SourceType[]} */ + const source_types = []; + getSourceTypes(mod, '', mod, source_types); - const imports = resolveImports(androidLibrary, mod.imports, mod.packageName, source_mtis); + const imports = resolveImports(androidLibrary, source_types, mod.imports, mod.packageName); + + source_types.forEach(t => { + resolveResolvableTypes(t, imports.resolved, imports.typemap); + }); const module_validaters = [ require('./validation/multiple-package-decls'), @@ -39,10 +62,15 @@ function validate(mod, androidLibrary) { require('./validation/parse-errors'), require('./validation/modifier-errors'), require('./validation/unresolved-imports'), - require('./validation/resolved-types'), + require('./validation/unresolved-types'), + require('./validation/invalid-types'), + require('./validation/bad-extends'), + require('./validation/bad-implements'), + require('./validation/non-implemented-interfaces'), + require('./validation/bad-overrides'), ]; let problems = [ - module_validaters.map(v => v(mod, imports)), + module_validaters.map(v => v(mod, imports, source_types)), ]; console.timeEnd('validation'); diff --git a/langserver/java/validation/bad-extends.js b/langserver/java/validation/bad-extends.js new file mode 100644 index 0000000..d3132bf --- /dev/null +++ b/langserver/java/validation/bad-extends.js @@ -0,0 +1,54 @@ +const { ModuleBlock, TypeDeclBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const {SourceType} = require('../source-type'); + +/** + * @param {SourceType} source_type + * @param {*} probs + */ +function checkExtends(source_type, probs) { + if (source_type.extends_types.length === 0) { + return; + } + const supertypes = source_type.extends_types.map(st => st.resolved); + const supertype = supertypes[0]; + if (source_type.typeKind === 'enum') { + probs.push(ParseProblem.Error(source_type.extends_types[0].typeTokens, `Enum types cannot declare a superclass`)); + } + if (source_type.typeKind === 'class' && supertypes.length > 1) { + probs.push(ParseProblem.Error(source_type.extends_types[1].typeTokens, `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].typeTokens, `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].typeTokens, `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].typeTokens, `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].typeTokens, `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].typeTokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from itself`)); + } + }) + } +} + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkExtends(type, probs)); + + return probs; +} diff --git a/langserver/java/validation/bad-implements.js b/langserver/java/validation/bad-implements.js new file mode 100644 index 0000000..99c8b68 --- /dev/null +++ b/langserver/java/validation/bad-implements.js @@ -0,0 +1,42 @@ +const { ModuleBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const {SourceType} = require('../source-type'); +const { UnresolvedType } = require('java-mti'); + +/** + * @param {SourceType} source_type + * @param {*} probs + */ +function checkImplements(source_type, probs) { + if (source_type.implements_types.length === 0) { + return; + } + const interfaces = source_type.implements_types.map(it => it.resolved); + if (source_type.typeKind === 'interface') { + probs.push(ParseProblem.Error(source_type.implements_types[0].typeTokens, `Interface types cannot declare an implements section`)); + } + if (source_type.typeKind === 'class') { + interfaces.forEach((intf, i) => { + if (intf instanceof UnresolvedType) { + return; + } + if (intf.typeKind !== 'interface') { + probs.push(ParseProblem.Error(source_type.implements_types[i].typeTokens, `Class '${source_type.fullyDottedRawName}' cannot implement ${intf.typeKind} type: '${intf.fullyDottedRawName}'`)); + } + }) + } +} + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkImplements(type, probs)); + + return probs; +} diff --git a/langserver/java/validation/bad-overrides.js b/langserver/java/validation/bad-overrides.js new file mode 100644 index 0000000..4d02dcb --- /dev/null +++ b/langserver/java/validation/bad-overrides.js @@ -0,0 +1,72 @@ +const { ModuleBlock, TextBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const {SourceType} = require('../source-type'); +const {CEIType, Method} = require('java-mti'); + +function nonAbstractLabel(label) { + return label.replace(/\babstract /g, ''); +} + +/** + * @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:TextBlock, method:Method, method_id:string}[]} */ + const overriden_methods = []; + source_type.methods.reduce((arr, method) => { + const ann = method._decl.annotations.find(a => /^@\s*Override$/.test(a.source)); + 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, `${x.method.label} does not override a matching method in any inherited type or interface`)); + } + }) +} + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkOverrides(type, probs)); + + return probs; +} diff --git a/langserver/java/validation/invalid-types.js b/langserver/java/validation/invalid-types.js new file mode 100644 index 0000000..a8ac6f4 --- /dev/null +++ b/langserver/java/validation/invalid-types.js @@ -0,0 +1,49 @@ +const { ModuleBlock, TypeDeclBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const {SourceType} = require('../source-type'); +const {JavaType, CEIType, TypeArgument, UnresolvedType} = require('java-mti') + +/** + * @param {JavaType} type + */ +function checkType(type, is_return_type, typeTokens, probs) { + if (type instanceof UnresolvedType) { + return; + } + const typesig = type.typeSignature; + if (typesig === 'V' && !is_return_type) { + probs.push(ParseProblem.Error(typeTokens, `'void' is not a valid type for variables`)) + } + if (/^\[+V/.test(typesig)) { + probs.push(ParseProblem.Error(typeTokens, `Illegal type: '${type.label}'`)) + } +} + +/** + * @param {SourceType} type + * @param {*} probs + */ +function checkInvalidTypes(type, probs) { + type.fields.forEach(f => checkType(f.type, false, f._decl.typeTokens, probs)); + type.methods.forEach(m => { + checkType(m.returnType, true, m._decl.typeTokens, probs); + m.parameters.forEach(p => { + checkType(p.type, false, p._decl.typeTokens, probs); + }) + }) +} + + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkInvalidTypes(type, probs)); + + return probs; +} diff --git a/langserver/java/validation/modifier-errors.js b/langserver/java/validation/modifier-errors.js index 54b5a81..44baf1d 100644 --- a/langserver/java/validation/modifier-errors.js +++ b/langserver/java/validation/modifier-errors.js @@ -16,10 +16,6 @@ function checkDuplicate(mods, probs) { m.set(mod.source, mod); } else { probs.push(ParseProblem.Error(mod, 'Duplicate modifier')); - if (firstmod !== null) { - probs.push(ParseProblem.Error(firstmod, 'Duplicate modifier')); - m.set(mod.source, null); - } } } } @@ -72,13 +68,15 @@ function checkFieldModifiers(field, probs) { } /** - * @param {Set} ownertypemods + * @param {TypeDeclBlock} type + * @param {Map} ownertypemods * @param {MethodBlock} method * @param {ParseProblem[]} probs */ -function checkMethodModifiers(ownertypemods, method, probs) { +function checkMethodModifiers(type, ownertypemods, method, probs) { checkDuplicate(method.modifiers, probs); checkConflictingAccess(method.modifiers, probs); + const allmods = new Map(method.modifiers.map(m => [m.source, m])); if (allmods.has('abstract') && allmods.has('final')) { probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and final')); @@ -89,10 +87,10 @@ function checkMethodModifiers(ownertypemods, method, probs) { if (allmods.has('abstract') && method.body().simplified.startsWith('B')) { probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations marked as abstract cannot have a method body')); } - if (!allmods.has('abstract') && !allmods.has('native') && !method.body().simplified.startsWith('B')) { + if (type.kind() !== 'interface' && !allmods.has('abstract') && !allmods.has('native') && !method.body().simplified.startsWith('B')) { probs.push(ParseProblem.Error(method, `Method '${method.name}' must have an implementation or be defined as abstract or native`)); } - if (allmods.has('abstract') && !ownertypemods.has('abstract')) { + if (type.kind() !== 'interface' && allmods.has('abstract') && !ownertypemods.has('abstract')) { probs.push(ParseProblem.Error(method, `Method '${method.name}' cannot be declared abstract inside a non-abstract type`)); } if (allmods.has('native') && method.body().simplified.startsWith('B')) { @@ -119,9 +117,27 @@ function checkInitialiserModifiers(initialiser, probs) { * @param {ParseProblem[]} probs */ function checkTypeModifiers(type, probs) { - const typemods = new Set(type.modifiers.map(m => m.source)); + const typemods = new Map(type.modifiers.map(m => [m.source, m])); + checkDuplicate(type.modifiers, probs); + + if (type.kind() === 'interface' && typemods.has('final')) { + probs.push(ParseProblem.Error(typemods.get('final'), 'Interface declarations cannot be marked as final')); + } + if (type.kind() === 'enum' && typemods.has('abstract')) { + probs.push(ParseProblem.Error(typemods.get('abstract'), 'Enum declarations cannot be marked as abstract')); + } + // top-level types cannot be private, protected or static + for (let mod of ['private','protected', 'static']) { + if (!type.outer_type && typemods.has(mod)) { + probs.push(ParseProblem.Error(typemods.get(mod), `Top-level declarations cannot be marked as ${mod}`)); + } + } + if (type.outer_type) { + checkConflictingAccess(type.modifiers, probs); + } + type.fields.forEach(field => checkFieldModifiers(field, probs)); - type.methods.forEach(method => checkMethodModifiers(typemods, method, probs)); + type.methods.forEach(method => checkMethodModifiers(type, typemods, method, probs)); type.constructors.forEach(ctr => checkConstructorModifiers(ctr, probs)); //type.initialisers.forEach(initer => checkInitModifiers(initer, probs)); // check enclosed types diff --git a/langserver/java/validation/non-implemented-interfaces.js b/langserver/java/validation/non-implemented-interfaces.js new file mode 100644 index 0000000..e0d8f98 --- /dev/null +++ b/langserver/java/validation/non-implemented-interfaces.js @@ -0,0 +1,64 @@ +const { ModuleBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const { SourceType } = require('../source-type'); +const {CEIType} = require('java-mti'); + +function nonAbstractLabel(label) { + return label.replace(/\babstract /g, ''); +} + +/** + * @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; + } + 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.typeKind === 'interface') { + interfaces.add(s); + } + if (s instanceof CEIType) { + 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 => { + const namedsig = `${m.name}${m.methodSignature}` + if (implemented.indexOf(namedsig) < 0) { + missing_methods.push(nonAbstractLabel(m.label)); + } + }) + if (missing_methods.length) { + probs.push(ParseProblem.Error(source_type._decl.kindToken, `Non-abstract ${source_type.typeKind} '${source_type.fullyDottedRawName}' does not implement the following methods from interface '${intf.fullyDottedRawName}':\n${missing_methods.join('\n')}`)); + } + }); +} + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkImplementedInterfaces(type, probs)); + + return probs; +} diff --git a/langserver/java/validation/parse-errors.js b/langserver/java/validation/parse-errors.js index b2b5533..259b922 100644 --- a/langserver/java/validation/parse-errors.js +++ b/langserver/java/validation/parse-errors.js @@ -24,6 +24,7 @@ function checkMethodParseErrors(method, probs) { */ module.exports = function(mod) { const probs = []; + mod.parseErrors.forEach(err => probs.push(ParseProblem.Error(err, `Invalid, incomplete or unsupported declaration`))); mod.types.forEach(type => checkTypeParseErrors(type, probs)); return probs; } diff --git a/langserver/java/validation/resolved-types.js b/langserver/java/validation/resolved-types.js deleted file mode 100644 index b9f3a29..0000000 --- a/langserver/java/validation/resolved-types.js +++ /dev/null @@ -1,255 +0,0 @@ -/** - * @typedef {import('../parsetypes/resolved-import')} ResolvedImport - */ -const { ModuleBlock, TypeDeclBlock, DeclaredVariableBlock, MethodBlock, ParameterBlock, TextBlock } = require('../parser9'); -const ParseProblem = require('../parsetypes/parse-problem'); -const { resolveTypes } = require('../type-resolver'); -const ResolvedType = require('../parsetypes/resolved-type'); - -/** - * @param {DeclaredVariableBlock|MethodBlock|TextBlock[] & {typeTokens: *[]}} decl - * @param {ResolvedType|ResolvedType[]} resolved - * @param {ParseProblem[]} probs - */ -function checkResolvedTypes(decl, resolved, probs) { - if (Array.isArray(resolved)) { - resolved.forEach(resolved => checkResolvedTypes(decl, resolved, probs)); - return; - } - if (resolved.error) { - probs.push(ParseProblem.Error(decl, resolved.error)); - return; - } - // the parser will detect varargs (...) on all variable declarations - if (decl instanceof DeclaredVariableBlock && decl.isVarArgs && !(decl instanceof ParameterBlock)) { - probs.push(ParseProblem.Error(decl.varBlock.varargs_token, `Variable-arity can only be applied to parameter declarations.`)); - } - // void arrays are illegal - if (/^void\[/.test(resolved.rawlabel)) { - probs.push(ParseProblem.Error(decl.typeTokens, `Invalid type: ${resolved.rawlabel}`)); - return; - } - // void can only be used for method declarations - if (resolved.rawlabel === 'void' && decl instanceof DeclaredVariableBlock) { - probs.push(ParseProblem.Error(decl.typeTokens, `'void' is not a valid type for fields, parameters or variables`)); - return; - } - // no primitive type arguments - if (resolved.isTypeArg && resolved.isPrimitive) { - probs.push(ParseProblem.Error(decl.typeTokens, `Primitive types cannot be used as type arguments.`)); - return; - } - switch (resolved.mtis.length) { - case 0: - probs.push(ParseProblem.Error(decl.typeTokens, `Unresolved type: '${resolved.rawlabel}'`)); - break; - case 1: - break; - default: - const matchlist = resolved.mtis.map(m => `'${m.fullyDottedRawName}'`).join(', '); - probs.push(ParseProblem.Error(decl.typeTokens, `Ambiguous type: '${resolved.rawlabel}'. Possible matches: ${matchlist}.`)); - break; - } - - // check type arguments - resolved.parts - .filter(typepart => typepart.typeargs) - .forEach(typepart => { - checkResolvedTypes(decl, typepart.typeargs, probs); - // check number of type arguments match - if (resolved.mtis.length === 1 && typepart.typeargs.length !== resolved.mtis[0].typevars.length) { - const msg = resolved.mtis[0].typevars.length === 0 - ? `Type '${resolved.mtis[0].fullyDottedRawName}' is not declared as a parameterized type and cannot be used with type arguments.` - : `Wrong number of type arguments for: '${resolved.mtis[0].fullyDottedRawName}'. Expected ${resolved.mtis[0].typevars.length} but found ${typepart.typeargs.length}.`; - probs.push(ParseProblem.Error(decl.typeTokens, msg)); - } - }); -} - -/** - * @param {string} outername - * @param {TypeDeclBlock} owner_type - * @param {''|'.'|'$'} qualifier - * @param {ResolvedImport[]} resolved_imports - * @param {Map} typemap - * @param {ParseProblem[]} probs - */ -function resolveFieldTypes(outername, owner_type, qualifier, resolved_imports, typemap, probs) { - const fieldtypes = owner_type.fields.map(f => f.type); - const fully_qualified_scope_name = `${outername}${qualifier}${owner_type.simpleName}`; - const resolved = resolveTypes(fieldtypes, fully_qualified_scope_name, resolved_imports, typemap); - owner_type.fields.forEach((field,i) => { - checkResolvedTypes(field, resolved[i], probs); - }) - // check enclosed types - owner_type.types.forEach(type => { - resolveFieldTypes(fully_qualified_scope_name, type, '$', resolved_imports, typemap, probs); - }); -} - -function extractTypeList(decl) { - if (!decl) { - return []; - } - const types = []; - const re = /[WD]( *[WDT.])*/g; - decl = decl.blockArray(); - const sm = decl.sourcemap(); - for (let m; m = re.exec(sm.simplified);) { - const start = sm.map[m.index], end = sm.map[m.index + m[0].length-1]; - const block_range = decl.blocks.slice(start, end+1); - const typename = block_range.map(b => b.source).join(''); - block_range.typename = typename; - block_range.typeTokens = block_range; - types.push(block_range); - } - return types; -} - -/** - * @param {string} outername - * @param {TypeDeclBlock} owner_type - * @param {''|'.'|'$'} qualifier - * @param {ResolvedImport[]} resolved_imports - * @param {Map} typemap - * @param {ParseProblem[]} probs - */ -function resolveExtends(outername, owner_type, qualifier, resolved_imports, typemap, probs) { - if (!owner_type.extends_token) { - return; - } - // the scope for extends and implements needs to include any type variables, but not enclosed types - const fully_qualified_scope_name = `${outername}${qualifier}${owner_type.simpleName}`; - if (!/^(class|interface)/.test(owner_type.kind())) { - probs.push(ParseProblem.Error(owner_type.extends_token, `extends declaration is not valid for ${owner_type.kind()} type: ${fully_qualified_scope_name}`)); - return; - } - const eit_types = extractTypeList(owner_type.extends_token); - const resolved = resolveTypes(eit_types.map(x => x.typename), fully_qualified_scope_name, resolved_imports, typemap); - eit_types.forEach((eit_type,i) => { - checkResolvedTypes(eit_type, resolved[i], probs); - }) - switch(owner_type.kind()) { - case 'class': - if (eit_types[0] && resolved[0].mtis.length === 1 && resolved[0].mtis[0].typeKind !== 'class') { - probs.push(ParseProblem.Error(eit_types[0], `Class '${fully_qualified_scope_name}' cannot extend from ${resolved[0].mtis[0].typeKind} type '${resolved[0].mtis[0].fullyDottedRawName}'`)); - } - if (eit_types.length > 1) { - probs.push(ParseProblem.Error(eit_types[1], `Class types cannot extend from more than one type`)); - } - break; - case "interface": - eit_types.forEach((eit_type, i) => { - const mti = resolved[i].mtis[0]; - if (resolved[i].mtis.length === 1 && mti.typeKind !== 'interface') { - probs.push(ParseProblem.Error(eit_type, `Interface '${fully_qualified_scope_name}' cannot extend from ${mti.typeKind} type '${mti.fullyDottedRawName}'`)); - } - // check for repeated types - if (resolved[i].mtis.length === 1) { - const name = resolved[i].mtis[0].fullyDottedRawName; - if (resolved.findIndex(r => r.mtis.length === 1 && r.mtis[0].fullyDottedRawName === name) < i) { - probs.push(ParseProblem.Error(eit_types[1], `Repeated type: ${name}`)); - } - } - }) - break; - } - // check enclosed types - owner_type.types.forEach(type => { - resolveExtends(fully_qualified_scope_name, type, '$', resolved_imports, typemap, probs); - }); -} - -/** - * @param {string} outername - * @param {TypeDeclBlock} owner_type - * @param {''|'.'|'$'} qualifier - * @param {ResolvedImport[]} resolved_imports - * @param {Map} typemap - * @param {ParseProblem[]} probs - */ -function resolveImplements(outername, owner_type, qualifier, resolved_imports, typemap, probs) { - if (!owner_type.implements_token) { - return; - } - const fully_qualified_scope_name = `${outername}${qualifier}${owner_type.simpleName}`; - if (!/class/.test(owner_type.kind())) { - probs.push(ParseProblem.Error(owner_type.implements_token, `implements declaration is not valid for ${owner_type.kind()} type: ${fully_qualified_scope_name}`)); - return; - } - const eit_types = extractTypeList(owner_type.implements_token); - // the scope for extends and implements needs to include any type variables, but not enclosed types - const resolved = resolveTypes(eit_types.map(x => x.typename), fully_qualified_scope_name, resolved_imports, typemap); - eit_types.forEach((eit_type,i) => { - checkResolvedTypes(eit_type, resolved[i], probs); - }) - eit_types.forEach((eit_type, i) => { - const mti = resolved[i].mtis[0]; - if (resolved[i].mtis.length === 1 && mti.typeKind !== 'interface') { - probs.push(ParseProblem.Error(eit_type, `Interface '${fully_qualified_scope_name}' cannot extend from ${mti.typeKind} type '${mti.fullyDottedRawName}'`)); - } - // check for repeated types - if (resolved[i].mtis.length === 1) { - const name = resolved[i].mtis[0].fullyDottedRawName; - if (resolved.findIndex(r => r.mtis.length === 1 && r.mtis[0].fullyDottedRawName === name) < i) { - probs.push(ParseProblem.Error(eit_types[1], `Repeated type: ${name}`)); - } - } - }) - // check enclosed types - owner_type.types.forEach(type => { - resolveImplements(fully_qualified_scope_name, type, '$', resolved_imports, typemap, probs); - }); -} - -/** - * @param {string} outername - * @param {TypeDeclBlock} owner_type - * @param {''|'.'|'$'} qualifier - * @param {ResolvedImport[]} resolved_imports - * @param {Map} typemap - * @param {ParseProblem[]} probs - */ -function resolveMethodTypes(outername, owner_type, qualifier, resolved_imports, typemap, probs) { - const method_type_names = []; - owner_type.methods.forEach(m => { - method_type_names.push(m.type); - m.parameters.forEach(p => { - method_type_names.push(p.type); - }); - }); - const fully_qualified_scope_name = `${outername}${qualifier}${owner_type.simpleName}`; - const resolved = resolveTypes(method_type_names, fully_qualified_scope_name, resolved_imports, typemap); - let i = 0; - owner_type.methods.forEach(method => { - checkResolvedTypes(method, resolved[i++], probs); - method.parameters.forEach((parameter, idx, arr) => { - checkResolvedTypes(parameter, resolved[i++], probs); - if (parameter.isVarArgs && idx !== arr.length-1) { - probs.push(ParseProblem.Error(parameter, `Variable-arity parameters must be declared last.`)); - } - }); - }) - // check enclosed types - owner_type.types.forEach(type => { - resolveMethodTypes(fully_qualified_scope_name, type, '$', resolved_imports, typemap, probs); - }); -} - -/** - * @param {ModuleBlock} mod - */ -module.exports = function(mod, imports) { - /** @type {ParseProblem[]} */ - const probs = []; - - mod.types.forEach(type => { - const qualifier = mod.packageName ? '.' : ''; - resolveExtends(mod.packageName, type, qualifier, imports.resolved, imports.typemap, probs); - resolveImplements(mod.packageName, type, qualifier, imports.resolved, imports.typemap, probs); - resolveFieldTypes(mod.packageName, type, qualifier, imports.resolved, imports.typemap, probs); - resolveMethodTypes(mod.packageName, type, qualifier, imports.resolved, imports.typemap, probs); - }); - - return probs; -} diff --git a/langserver/java/validation/unresolved-imports.js b/langserver/java/validation/unresolved-imports.js index 803dfad..cf57a17 100644 --- a/langserver/java/validation/unresolved-imports.js +++ b/langserver/java/validation/unresolved-imports.js @@ -3,13 +3,14 @@ const ParseProblem = require('../parsetypes/parse-problem'); /** * @param {ModuleBlock} mod + * @param {{unresolved:*[]}} imports */ module.exports = function(mod, imports) { /** @type {ParseProblem[]} */ const probs = []; - imports.unresolved.forEach(i => { - probs.push(ParseProblem.Warning(i, `Unresolved import: ${i.name}`)); + imports.unresolved.forEach(import_tokens => { + probs.push(ParseProblem.Warning(import_tokens, `Unresolved import: ${import_tokens.name}`)); }) return probs; diff --git a/langserver/java/validation/unresolved-types.js b/langserver/java/validation/unresolved-types.js new file mode 100644 index 0000000..ce1ef3a --- /dev/null +++ b/langserver/java/validation/unresolved-types.js @@ -0,0 +1,52 @@ +const { ModuleBlock, TypeDeclBlock } = require('../parser9'); +const ParseProblem = require('../parsetypes/parse-problem'); +const {SourceType} = require('../source-type'); +const {JavaType, CEIType, TypeArgument, UnresolvedType} = require('java-mti') + +/** + * @param {JavaType} type + */ +function checkType(type, typeTokens, probs) { + if (type instanceof UnresolvedType) { + probs.push(ParseProblem.Error(typeTokens, `Unknown type: ${type.label}`)); + return; + } + if (type instanceof CEIType) { + type.typevars.forEach(tv => { + if (tv instanceof TypeArgument) { + checkType(tv.type, typeTokens, probs); + } + }) + } +} + +/** + * @param {SourceType} type + * @param {*} probs + */ +function checkUnresolvedTypes(type, probs) { + type.extends_types.forEach(superclass => checkType(superclass.resolved, superclass.typeTokens, probs)); + type.implements_types.forEach(superintf => checkType(superintf.resolved, superintf.typeTokens, probs)); + type.fields.forEach(f => checkType(f.type, f._decl.typeTokens, probs)); + type.methods.forEach(m => { + checkType(m.returnType, m._decl.typeTokens, probs); + m.parameters.forEach(p => { + checkType(p.type, p._decl.typeTokens, probs); + }) + }) +} + + +/** + * @param {ModuleBlock} mod + * @param {*} imports + * @param {SourceType[]} source_types + */ +module.exports = function(mod, imports, source_types) { + /** @type {ParseProblem[]} */ + const probs = []; + + source_types.forEach(type => checkUnresolvedTypes(type, probs)); + + return probs; +} diff --git a/langserver/package-lock.json b/langserver/package-lock.json index dfc0e92..7555ffb 100644 --- a/langserver/package-lock.json +++ b/langserver/package-lock.json @@ -1,6 +1,6 @@ { "name": "langserver", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,13 +10,249 @@ "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==", "dev": true }, - "jarscanner": { - "version": "file:../../../nodejs/jarscanner", + "java-mti": { + "version": "file:../../../nodejs/java-mti", + "requires": { + "unzipper": "^0.10.11" + }, "dependencies": { - "@types/node": { - "version": "13.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", - "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==" + "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=" + }, + "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" + } + }, + "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==" + }, + "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=" + }, + "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==" + }, + "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" + } + }, + "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=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } }, diff --git a/langserver/package.json b/langserver/package.json index 7a90121..0585542 100644 --- a/langserver/package.json +++ b/langserver/package.json @@ -1,6 +1,6 @@ { "name": "langserver", - "version": "1.0.0", + "version": "1.0.1", "description": "Java language server for Android development", "main": "server.js", "scripts": { @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "jarscanner": "file://~/dev/nodejs/jarscanner", + "java-mti": "file:../../../nodejs/java-mti", "vscode-languageserver": "^6.1.1", "vscode-languageserver-textdocument": "^1.0.1" }, diff --git a/langserver/server.js b/langserver/server.js index b0bd066..8898de0 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -17,46 +17,17 @@ const { const { TextDocument } = require('vscode-languageserver-textdocument'); -const MTI = require('./java/mti'); +const { loadAndroidLibrary, JavaType } = require('java-mti'); + const { ParseProblem } = require('./java/parser'); const { parse, ModuleBlock } = require('./java/parser9'); const { validate } = require('./java/validater'); +/** + * @typedef {Map} AndroidLibrary + * @type {AndroidLibrary|Promise} + */ let androidLibrary = null; -function loadAndroidLibrary(retry) { - try { - androidLibrary = MTI.unpackJSON('/tmp/jarscanner/android-25/android-25.json'); - connection.console.log(`Android type cache loaded: ${androidLibrary.types.length} types from ${androidLibrary.packages.length} packages.`); - } catch (e) { - connection.console.log(`Failed to load android type cache`); - if (retry) { - return; - } - connection.console.log(`Rebuilding type cache...`); - const jarscanner = require(`jarscanner/jarscanner`); - fs.mkdir('/tmp/jarscanner', (err) => { - if (err && err.errno !== -17) { - connection.console.log(`Cannot create type cache folder. ${err.message}.`); - return; - } - jarscanner.process_android_sdk_source( - { - destpath: '/tmp/jarscanner', - sdkpath: process.env['ANDROID_SDK'], - api: 25, - cleandest: true, - }, - (err) => { - if (err) { - connection.console.log(`Android cache build failed. ${err.message}.`); - return; - } - loadAndroidLibrary(true); - } - ); - }); - } -} // Create a connection for the server. The connection uses Node's IPC as a transport. // Also include all preview / proposed LSP features. @@ -144,7 +115,7 @@ let documents = new TextDocuments({ * @param {number} version */ update(document, changes, version) { - connection.console.log(JSON.stringify({ what: 'update', changes, version })); + connection.console.log(JSON.stringify({ what: 'update', /* changes, */ version })); //connection.console.log(`update ${version}`); //return document; if (parsed && document && parsed.uri === document.uri) { @@ -170,7 +141,14 @@ let hasWorkspaceFolderCapability = false; let hasDiagnosticRelatedInformationCapability = false; connection.onInitialize((params) => { - process.nextTick(loadAndroidLibrary); + console.time('android-library-load') + androidLibrary = loadAndroidLibrary('android-25').then(lib => { + console.timeEnd('android-library-load') + return androidLibrary = lib; + }, err => { + console.log(`android library load failed: ${err.message}`); + return androidLibrary = new Map(); + }); let capabilities = params.capabilities; // Does the client support the `workspace/configuration` request? @@ -266,12 +244,20 @@ documents.onDidChangeContent((change) => { * @param {{uri}} textDocument */ async function validateTextDocument(textDocument) { + if (androidLibrary instanceof Promise) { + connection.console.log('Waiting for Android Library load'); + androidLibrary = await androidLibrary; + } /** @type {ParseProblem[]} */ let problems = []; connection.console.log('validateTextDocument'); if (parsed && parsed.result) { - problems = validate(parsed.result, androidLibrary); + try { + problems = validate(parsed.result, androidLibrary); + } catch(err) { + console.error(err); + } } const diagnostics = problems @@ -353,10 +339,13 @@ connection.onCompletion( /** * @param {*} _textDocumentPosition TextDocumentPositionParams */ - (_textDocumentPosition) => { + async (_textDocumentPosition) => { // The pass parameter contains the position of the text document in // which code complete got requested. For the example we ignore this // info and always provide the same completion items. + if (androidLibrary instanceof Promise) { + androidLibrary = await androidLibrary; + } const lib = androidLibrary; if (!lib) return []; const typeKindMap = { @@ -383,13 +372,13 @@ connection.onCompletion( kind: CompletionItemKind.Value, data: -1, })), - ...lib.types.map( + ...[...lib.values()].map( (t, idx) => /** @type {CompletionItem} */ ({ - label: t.dottedRawName, + label: t.dottedTypeName, kind: typeKindMap[t.typeKind], - data: idx, + data: t.shortSignature, }) ), ]) @@ -404,14 +393,17 @@ connection.onCompletionResolve( * @param {CompletionItem} item */ (item) => { - const t = androidLibrary.types[item.data]; + if (androidLibrary instanceof Promise) { + return item; + } + const t = androidLibrary.get(item.data); if (!t) { return item; } - item.detail = `${t.package}.${t.dottedRawName}`; + item.detail = t.fullyDottedRawName; item.documentation = t.docs && { kind: 'markdown', - value: `${t.typeKind} **${t.dottedName}**\n\n${ + value: `${t.typeKind} **${t.dottedTypeName}**\n\n${ t.docs .replace(/(

)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>)|(\{@link.+?\}|\{@code.+?\})|(

  • )|(.+?<\/a>)|()|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,p,i,b,tt,c,lc,li,a,h) => { return p ? '\n\n'