mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-25 10:58:42 +00:00
first hacky version of source parsing and type checking
This commit is contained in:
16
langserver/java/parsetypes/annotation.js
Normal file
16
langserver/java/parsetypes/annotation.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @typedef {import('./token')} Token
|
||||
*/
|
||||
|
||||
class Annotation {
|
||||
/**
|
||||
* @param {Token} at
|
||||
* @param {Token} name
|
||||
*/
|
||||
constructor(at, name) {
|
||||
this.at = at;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Annotation;
|
||||
49
langserver/java/parsetypes/declaration.js
Normal file
49
langserver/java/parsetypes/declaration.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const Token = require('./token');
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./type')} TypeDeclaration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for Java declarations.
|
||||
*/
|
||||
class Declaration {
|
||||
/**
|
||||
* @param {TypeDeclaration} owner_type the type this declaration belongs to (if any)
|
||||
* @param {Token} docs JavaDocs associated with the declaration
|
||||
* @param {Modifier[]} modifiers annotations, modifier keywords and type parameters
|
||||
*/
|
||||
constructor(owner_type, docs, modifiers) {
|
||||
this.owner_type = owner_type;
|
||||
this.docs = docs;
|
||||
this.modifiers = modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the raw JavaDoc string or an empty string if no doc is present
|
||||
*/
|
||||
getDocString() {
|
||||
return this.docs ? this.docs.text : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw access modifier text values
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAccessModifierValues() {
|
||||
// @ts-ignore
|
||||
return this.modifiers.filter(m => m instanceof Token).map(t => t.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the token matching the specified modifier
|
||||
* @param {string} name
|
||||
* @returns {Token}
|
||||
*/
|
||||
findModifier(name) {
|
||||
// @ts-ignore
|
||||
return this.modifiers.find(m => (m instanceof Token) && (m.text === name));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Declaration;
|
||||
90
langserver/java/parsetypes/fmc.js
Normal file
90
langserver/java/parsetypes/fmc.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./parameter')} ParameterDeclaration
|
||||
* @typedef {import('./token')} Token
|
||||
* @typedef {import('./type')} TypeDeclaration
|
||||
* @typedef {import('./typeident')} TypeIdent
|
||||
*/
|
||||
const Declaration = require('./declaration');
|
||||
const ParseProblem = require('./parse-problem');
|
||||
const ProblemSeverity = require('./problem-severity');
|
||||
|
||||
/**
|
||||
* Field, method or constructor declaration
|
||||
*/
|
||||
class FMCDeclaration extends Declaration {
|
||||
/**
|
||||
*
|
||||
* @param {TypeDeclaration} owner_type
|
||||
* @param {Token} docs
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {'field'|'method'|'constructor'} kind
|
||||
* @param {Token} name
|
||||
* @param {TypeIdent} type
|
||||
* @param {Token} equals_comma_sc
|
||||
* @param {ParameterDeclaration[]} parameters
|
||||
*/
|
||||
constructor(owner_type, docs, modifiers, kind, name, type, equals_comma_sc, parameters) {
|
||||
super(owner_type, docs, modifiers);
|
||||
this.kind = kind;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.equals_comma_sc = equals_comma_sc;
|
||||
this.parameters = parameters || [];
|
||||
}
|
||||
|
||||
validate() {
|
||||
const checkDuplicateParameterNames = () => {
|
||||
const done = new Set();
|
||||
return this.parameters
|
||||
.filter(p => {
|
||||
if (done.has(p.name.text)) {
|
||||
return true;
|
||||
}
|
||||
done.add(p.name.text);
|
||||
})
|
||||
.map(p =>
|
||||
new ParseProblem(p.name, `Duplicate parameter name: '${p.name.text}'`, ProblemSeverity.Error)
|
||||
);
|
||||
};
|
||||
const checkParameterCommas = () => {
|
||||
const last_param_idx = this.parameters.length - 1;
|
||||
return this.parameters.map((p, idx) => {
|
||||
if ((idx < last_param_idx) && !p.comma) {
|
||||
return new ParseProblem(p.lastToken(), 'Missing comma', ProblemSeverity.Error);
|
||||
}
|
||||
else if ((idx === last_param_idx) && p.comma) {
|
||||
return ParseProblem.syntaxError(p.comma);
|
||||
}
|
||||
});
|
||||
}
|
||||
const checkFieldSemicolon = () => {
|
||||
if (this.kind === 'field') {
|
||||
if (!this.equals_comma_sc) {
|
||||
return new ParseProblem(this.name, `Missing operator or semicolon`, ProblemSeverity.Error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const checkVarargsIsLastParameter = () => {
|
||||
return this.parameters
|
||||
.slice(0, -1)
|
||||
.filter(p => p.varargs)
|
||||
.map(p =>
|
||||
new ParseProblem(p.varargs, 'A variable arity parameter must be declared last', ProblemSeverity.Error)
|
||||
);
|
||||
};
|
||||
const problems = [
|
||||
...ParseProblem.checkAccessModifiers(this.modifiers, this.kind),
|
||||
...ParseProblem.checkDuplicateModifiers(this.modifiers),
|
||||
...ParseProblem.checkConflictingModifiers(this.modifiers),
|
||||
...checkParameterCommas(),
|
||||
...checkDuplicateParameterNames(),
|
||||
...checkVarargsIsLastParameter(),
|
||||
checkFieldSemicolon(),
|
||||
];
|
||||
return problems;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FMCDeclaration;
|
||||
68
langserver/java/parsetypes/import.js
Normal file
68
langserver/java/parsetypes/import.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const Declaration = require('./declaration');
|
||||
const ParseProblem = require('./parse-problem');
|
||||
const Token = require('./token');
|
||||
const TypeParameters = require('./type-parameters');
|
||||
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
*/
|
||||
|
||||
class ImportDeclaration extends Declaration {
|
||||
/**
|
||||
* @param {Token} docs
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {Token[]} nameparts
|
||||
* @param {Token} static_
|
||||
* @param {Token} asterisk
|
||||
* @param {Token} semicolon
|
||||
*/
|
||||
constructor(docs, modifiers, nameparts, static_, asterisk, semicolon) {
|
||||
super(null, docs, modifiers);
|
||||
this.nameparts = nameparts;
|
||||
this.static_ = static_;
|
||||
this.asterisk = asterisk;
|
||||
this.semicolon = semicolon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dotted portion of the import declaration (excluding any demand-load part)
|
||||
*/
|
||||
getDottedName() {
|
||||
return this.nameparts.map(x => x.text).join('.');
|
||||
}
|
||||
|
||||
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;
|
||||
15
langserver/java/parsetypes/modifier.js
Normal file
15
langserver/java/parsetypes/modifier.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @typedef {import('./annotation')} Annotation
|
||||
* @typedef {import('./type-parameters')} TypeParameters
|
||||
* @typedef {import('./token')} Token
|
||||
*
|
||||
* Each Modifier is one of
|
||||
* - a token representing a modifier keyword (e.g public, static, etc)
|
||||
* - an Annotation (eg. @Override)
|
||||
* - or a TypeParameters section (eg <T extends Object>)
|
||||
* These can typically appear in any order before a declaration
|
||||
*
|
||||
* @typedef {Token|Annotation|TypeParameters} Modifier
|
||||
*/
|
||||
|
||||
module.exports = {}
|
||||
39
langserver/java/parsetypes/package.js
Normal file
39
langserver/java/parsetypes/package.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const Declaration = require('./declaration');
|
||||
const ParseProblem = require('./parse-problem');
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./token')} Token
|
||||
*/
|
||||
|
||||
class PackageDeclaration extends Declaration {
|
||||
/**
|
||||
* @param {Token} docs
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {Token[]} nameparts
|
||||
* @param {Token} semicolon
|
||||
*/
|
||||
constructor(docs, modifiers, nameparts, semicolon) {
|
||||
super(null, docs, modifiers);
|
||||
this.nameparts = nameparts;
|
||||
this.semicolon = semicolon;
|
||||
}
|
||||
|
||||
dottedName() {
|
||||
return this.nameparts.map(t => t.text).join('.');
|
||||
}
|
||||
|
||||
lastToken() {
|
||||
return this.semicolon || this.nameparts.slice(-1)[0];
|
||||
}
|
||||
|
||||
validate() {
|
||||
/** @type {ParseProblem[]} */
|
||||
const problems = [
|
||||
ParseProblem.checkSemicolon(this),
|
||||
...ParseProblem.checkNonKeywordIdents(this.nameparts),
|
||||
];
|
||||
return problems;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PackageDeclaration;
|
||||
33
langserver/java/parsetypes/parameter.js
Normal file
33
langserver/java/parsetypes/parameter.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const Declaration = require('./declaration');
|
||||
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./typeident')} TypeIdent
|
||||
* @typedef {import('./token')} Token
|
||||
*/
|
||||
|
||||
/**
|
||||
* A single parameter declaration
|
||||
*/
|
||||
class ParameterDeclaration extends Declaration {
|
||||
/**
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {TypeIdent} type
|
||||
* @param {Token} varargs
|
||||
* @param {Token} name
|
||||
* @param {Token} comma
|
||||
*/
|
||||
constructor(modifiers, type, varargs, name, comma) {
|
||||
super(null, null, modifiers);
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.varargs = varargs;
|
||||
this.comma = comma;
|
||||
}
|
||||
|
||||
lastToken() {
|
||||
return this.comma || this.name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParameterDeclaration;
|
||||
29
langserver/java/parsetypes/parse-error.js
Normal file
29
langserver/java/parsetypes/parse-error.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const Declaration = require('./declaration');
|
||||
const ParseProblem = require('./parse-problem');
|
||||
/**
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./token')} Token
|
||||
*/
|
||||
|
||||
class ParseSyntaxError extends Declaration {
|
||||
/**
|
||||
* @param {Token} docs
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {Token} errorToken
|
||||
*/
|
||||
constructor(docs, modifiers, errorToken) {
|
||||
super(null, docs, modifiers);
|
||||
this.errorToken = errorToken;
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.errorToken) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
ParseProblem.syntaxError(this.errorToken),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParseSyntaxError;
|
||||
134
langserver/java/parsetypes/parse-problem.js
Normal file
134
langserver/java/parsetypes/parse-problem.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const ProblemSeverity = require('./problem-severity');
|
||||
const Token = require('./token');
|
||||
|
||||
/**
|
||||
* @typedef {import('./import')} ImportDeclaration
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./package')} PackageDeclaration
|
||||
* @typedef {import('./problem-severity').Severity} Severity
|
||||
*/
|
||||
|
||||
|
||||
class ParseProblem {
|
||||
/**
|
||||
* @param {Token|Token[]} token
|
||||
* @param {string} message
|
||||
* @param {Severity} severity
|
||||
*/
|
||||
constructor(token, message, severity) {
|
||||
this.startIdx = (Array.isArray(token) ? token[0] : token).source_idx;
|
||||
const lastToken = (Array.isArray(token) ? token[token.length - 1] : token);
|
||||
this.endIdx = lastToken.source_idx + lastToken.text.length;
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Modifier[]} mods
|
||||
*/
|
||||
static checkDuplicateModifiers(mods) {
|
||||
const done = new Set();
|
||||
const res = [];
|
||||
for (let mod of mods) {
|
||||
if (mod instanceof Token) {
|
||||
if (done.has(mod.text)) {
|
||||
res.push(new ParseProblem(mod, `Duplicate modifier: ${mod.text}`, ProblemSeverity.Error));
|
||||
}
|
||||
done.add(mod.text);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static checkConflictingModifiers(mods) {
|
||||
const modmap = new Map();
|
||||
let res = [];
|
||||
mods.filter(m => m instanceof Token).forEach(m => modmap.set(m.text, m));
|
||||
const names = [...modmap.keys()];
|
||||
const visibilities = names.filter(m => /^(public|private|protected)$/.test(m));
|
||||
if (visibilities.length > 1) {
|
||||
const visnames = visibilities.map(m => `'${m}'`).join(', ').replace(/, (?='\w+'$)/, ' and ');
|
||||
res = visibilities.map(m => new ParseProblem(modmap.get(m), `Conflicting modifiers: ${visnames}`, ProblemSeverity.Error));
|
||||
}
|
||||
if (names.includes('abstract')) {
|
||||
if (names.includes('final')) {
|
||||
res.push(new ParseProblem(modmap.get('final'), `Declarations cannot be both 'abstract' and 'final`, ProblemSeverity.Error));
|
||||
}
|
||||
if (names.includes('native')) {
|
||||
res.push(new ParseProblem(modmap.get('native'), `Declarations cannot be both 'abstract' and 'native`, ProblemSeverity.Error));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Modifier[]} mods
|
||||
* @param {'class'|'interface'|'enum'|'@interface'|'field'|'method'|'constructor'|'initializer'} decl_kind
|
||||
*/
|
||||
static checkAccessModifiers(mods, decl_kind) {
|
||||
let valid_mods = /^$/;
|
||||
switch (decl_kind) {
|
||||
case 'class': valid_mods = /^(public|final|abstract|strictfp)$/; break;
|
||||
case 'interface': valid_mods = /^(public|abstract|strictfp)$/; break;
|
||||
case '@interface': valid_mods = /^(public)$/; break;
|
||||
case 'enum': valid_mods = /^(public|final)$/; break;
|
||||
case 'field': valid_mods = /^(public|private|protected|static|final|volatile|transient)$/; break;
|
||||
case 'method': valid_mods = /^(public|private|protected|static|final|abstract|native|strictfp|synchronized)$/; break;
|
||||
case 'constructor': valid_mods = /^(public|protected|native)$/; break;
|
||||
case 'initializer': valid_mods = /^(static)$/; break;
|
||||
}
|
||||
const problems = [];
|
||||
for (let mod of mods) {
|
||||
if (mod instanceof Token) {
|
||||
if (!valid_mods.test(mod.text)) {
|
||||
problems.push(new ParseProblem(mod, `'${mod.text}' is not a valid modifier for ${decl_kind} type declarations`, ProblemSeverity.Warning));
|
||||
}
|
||||
const redundant = (mod.text === 'abstract' && decl_kind === 'interface')
|
||||
|| (mod.text === 'final' && decl_kind === 'enum');
|
||||
if (redundant) {
|
||||
problems.push(new ParseProblem(mod, `'${mod.text}' is redundant for a ${decl_kind} declaration`, ProblemSeverity.Hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PackageDeclaration|ImportDeclaration} o
|
||||
*/
|
||||
static checkSemicolon(o) {
|
||||
if (!o.semicolon) {
|
||||
const lastToken = o.lastToken();
|
||||
return new ParseProblem(lastToken, 'Missing operator or semicolon', ProblemSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
static checkNonKeywordIdents(tokens) {
|
||||
const res = [];
|
||||
const KEYWORDS = /^(abstract|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|final|finally|for|goto|if|implements|import|interface|native|new|package|private|protected|public|return|static|strictfp|super|switch|synchronized|throw|throws|transient|try|volatile|while)$/;
|
||||
const PRIMITIVE_TYPE_KEYWORDS = /^(int|boolean|byte|char|double|float|long|short|void)$/
|
||||
const LITERAL_VALUE_KEYWORDS = /^(this|true|false|null)$/;
|
||||
const OPERATOR_KEYWORDS = /^(instanceof)$/;
|
||||
for (let token of tokens) {
|
||||
let iskw = KEYWORDS.test(token.text) || PRIMITIVE_TYPE_KEYWORDS.test(token.text) || LITERAL_VALUE_KEYWORDS.test(token.text) || OPERATOR_KEYWORDS.test(token.text);
|
||||
if (iskw) {
|
||||
const problem = new ParseProblem(token, `'${token.text}' is a keyword and cannot be used as an identifier`, ProblemSeverity.Error);
|
||||
res.push(problem);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
static syntaxError(token) {
|
||||
if (!token) return null;
|
||||
return new ParseProblem(token, 'Unsupported, invalid or incomplete declaration', ProblemSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParseProblem;
|
||||
24
langserver/java/parsetypes/parse-result.js
Normal file
24
langserver/java/parsetypes/parse-result.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @typedef {import('./import')} ImportDeclaration
|
||||
* @typedef {import('./package')} PackageDeclaration
|
||||
* @typedef {import('./parse-error')} ParseSyntaxError
|
||||
* @typedef {import('./type')} TypeDeclaration
|
||||
*/
|
||||
|
||||
class ParseResult {
|
||||
/**
|
||||
*
|
||||
* @param {PackageDeclaration} package_decl
|
||||
* @param {ImportDeclaration[]} imports
|
||||
* @param {TypeDeclaration[]} types
|
||||
* @param {ParseSyntaxError[]} invalids
|
||||
*/
|
||||
constructor(package_decl, imports, types, invalids) {
|
||||
this.package = package_decl;
|
||||
this.imports = imports;
|
||||
this.types = types;
|
||||
this.invalids = invalids;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParseResult;
|
||||
8
langserver/java/parsetypes/problem-severity.js
Normal file
8
langserver/java/parsetypes/problem-severity.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @typedef {1|2|3|4} Severity
|
||||
* @type {{ Error:1, Warning:2, Information:3, Hint:4 }}
|
||||
* these match the vscode DiagnosticSeverity values
|
||||
*/
|
||||
const ProblemSeverity = { Error:1, Warning:2, Information:3, Hint:4 };
|
||||
|
||||
module.exports = ProblemSeverity;
|
||||
45
langserver/java/parsetypes/resolved-import.js
Normal file
45
langserver/java/parsetypes/resolved-import.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @typedef {import('./import')} ImportDeclaration
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
*/
|
||||
class ResolvedImport {
|
||||
/**
|
||||
* @param {ImportDeclaration} import_decl
|
||||
* @param {RegExpMatchArray} matches
|
||||
* @param {'owner-package'|'import'|'implicit-import'} import_kind;
|
||||
*/
|
||||
constructor(import_decl, matches, typemap, import_kind) {
|
||||
/**
|
||||
* The associated import declaration.
|
||||
* - this value is null for owner-package and implicit-imports
|
||||
*/
|
||||
this.import = import_decl;
|
||||
|
||||
/**
|
||||
* Array of fully qualified type names in JRE format resolved in this import
|
||||
*/
|
||||
this.fullyQualifiedNames = Array.from(matches);
|
||||
|
||||
/**
|
||||
* THe map of fully-qualified type names to MTIs
|
||||
*/
|
||||
this.types = new Map(matches.map(name => [name, typemap.get(name)]));
|
||||
|
||||
/**
|
||||
* What kind of import this is:
|
||||
* - `"owner-package"`: types that are implicitly imported from the same package as the declared module
|
||||
* - `"import"`: types that are inclduded via an import declaration specified in the module
|
||||
* - `"implicit-import"`: types that are included without any explicit import (`java.lang.*` for example)
|
||||
*/
|
||||
this.import_kind = import_kind;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResolvedImport;
|
||||
105
langserver/java/parsetypes/resolved-type.js
Normal file
105
langserver/java/parsetypes/resolved-type.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @typedef {import('./token')} Token
|
||||
* @typedef {import('./type')} TypeDeclaration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a parsed and resolved type
|
||||
*
|
||||
* Each `ResolvedType` consists of a linked set of parsed `TypeParts` and an array dimensions count.
|
||||
* Each `TypePart` is a single dotted type with optional type arguments.
|
||||
*
|
||||
* When parsing, the first type part matches all dotted idents up to the first type with arguments - after
|
||||
* that, there is a single type part for each further enclosed type.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* int -> one TypePart, arrdims = 0
|
||||
* int[][] -> one TypePart, arrdims = 2
|
||||
* List<String> -> one type part with one typeargs entry
|
||||
* List<String>.InnerType -> two type parts (List<String> / InnerType)
|
||||
* List<String>.InnerType.AnotherInner -> three type parts (List<String> / InnerType / AnotherInner)
|
||||
* java.util.List<String>.InnerType<Object>.AnotherInner -> three type parts (java.util.List<String> / InnerType<Object> / AnotherInner)
|
||||
* java.util.List.InnerType.AnotherInner -> one type part
|
||||
*
|
||||
* The reason for the non-obvious splitting is that the first part of the type could incorporate a package name - we
|
||||
* cannot tell which parts of the name are packages and which are types/enclosed types until we try to resolve it.
|
||||
* But type arguments are only allowed on types, so any qualifiers that appear after type arguments can only be a type and
|
||||
* so we split on each single identifier.
|
||||
*
|
||||
*/
|
||||
class ResolvedType {
|
||||
|
||||
static TypePart = class TypePart {
|
||||
/**
|
||||
* The list of type arguments
|
||||
* @type {ResolvedType[]}
|
||||
*/
|
||||
typeargs = null;
|
||||
|
||||
/**
|
||||
* The outer type if this is an enclosed generic type
|
||||
* @type {ResolvedType.TypePart}
|
||||
*/
|
||||
outer = null;
|
||||
inner = null;
|
||||
|
||||
/**
|
||||
* @param {ResolvedType} owner
|
||||
* @param {string} name
|
||||
* @param {ResolvedType.TypePart} outer
|
||||
*/
|
||||
constructor(owner, name, outer) {
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
this.outer = outer;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.name + (this.typeargs ? `<${this.typeargs.map(arg => arg.label).join(',')}>` : '');
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ResolvedType.TypePart[]} */
|
||||
parts = [];
|
||||
|
||||
/**
|
||||
* number of array dimensions for this type
|
||||
*/
|
||||
arrdims = 0;
|
||||
|
||||
/**
|
||||
* Error reason if parsing failed.
|
||||
*/
|
||||
error = '';
|
||||
|
||||
/**
|
||||
* The resolved MTIs that match this type. This will be an empty array if the type cannot be found.
|
||||
* @type {import('../mti').Type[]}
|
||||
*/
|
||||
mtis = [];
|
||||
|
||||
/**
|
||||
* During parsing, add a new type part
|
||||
* @param {string} [name]
|
||||
* @param {ResolvedType.TypePart} [outer]
|
||||
*/
|
||||
addTypePart(name = '', outer = null) {
|
||||
const p = new ResolvedType.TypePart(this, name, outer);
|
||||
this.parts.push(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
getDottedRawType() {
|
||||
// most types will only have one part
|
||||
if (this.parts.length === 1)
|
||||
return this.parts[0].name;
|
||||
return this.parts.map(p => p.name).join('.');
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.parts.map(p => p.label).join('.') + '[]'.repeat(this.arrdims);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ResolvedType;
|
||||
17
langserver/java/parsetypes/token.js
Normal file
17
langserver/java/parsetypes/token.js
Normal file
@@ -0,0 +1,17 @@
|
||||
class Token {
|
||||
/**
|
||||
*
|
||||
* @param {number} source_idx
|
||||
* @param {string} text
|
||||
* @param {string} simplified_text
|
||||
* @param {number} simplified_text_idx
|
||||
*/
|
||||
constructor(source_idx, text, simplified_text, simplified_text_idx) {
|
||||
this.source_idx = source_idx;
|
||||
this.text = text;
|
||||
this.simplified_text = simplified_text;
|
||||
this.simplified_text_idx = simplified_text_idx;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Token;
|
||||
17
langserver/java/parsetypes/type-parameters.js
Normal file
17
langserver/java/parsetypes/type-parameters.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('./token')} Token
|
||||
*/
|
||||
|
||||
class TypeParameters {
|
||||
/**
|
||||
*
|
||||
* @param {Token} open
|
||||
* @param {Token} close
|
||||
*/
|
||||
constructor(open, close) {
|
||||
this.open = open;
|
||||
this.close = close;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TypeParameters;
|
||||
229
langserver/java/parsetypes/type.js
Normal file
229
langserver/java/parsetypes/type.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const Declaration = require('./declaration');
|
||||
const ParseProblem = require('./parse-problem');
|
||||
const ProblemSeverity = require('./problem-severity');
|
||||
const ResolvedImport = require('../import-resolver').ResolvedImport;
|
||||
const { resolveTypeIdents } = require('../type-resolver');
|
||||
const Token = require('./token');
|
||||
|
||||
/**
|
||||
* @typedef {import('./import')} ImportDeclaration
|
||||
* @typedef {import('./fmc')} FMCDeclaration
|
||||
* @typedef {import('./modifier')} Modifier
|
||||
* @typedef {import('./parameter')} ParameterDeclaration
|
||||
* @typedef {import('./typeident')} TypeIdent
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a single Java type (class, interface, enum or @-interface) declaration
|
||||
*/
|
||||
class TypeDeclaration extends Declaration {
|
||||
/**
|
||||
*
|
||||
* @param {TypeDeclaration} owner_type
|
||||
* @param {Token} docs
|
||||
* @param {Modifier[]} modifiers
|
||||
* @param {'class'|'interface'|'enum'|'@interface'} kind
|
||||
* @param {Token} name
|
||||
*/
|
||||
constructor(owner_type, docs, modifiers, kind, name) {
|
||||
super(owner_type, docs, modifiers);
|
||||
this.kind = kind;
|
||||
this.name = name;
|
||||
/** @type {FMCDeclaration[]} */
|
||||
this.declarations = [];
|
||||
/** @type {{decl_kw:Token, typelist:TypeIdent[]}[]} */
|
||||
this.super_declarations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the $-qualified name of this type (excluding package)
|
||||
*/
|
||||
qualifiedName() {
|
||||
if (!this.owner_type) {
|
||||
// top-level type
|
||||
return this.name.text;
|
||||
}
|
||||
const parts = [];
|
||||
for (let t = this; t;) {
|
||||
parts.unshift(t.name.text);
|
||||
// @ts-ignore
|
||||
t = t.owner_type;
|
||||
}
|
||||
return parts.join('$');
|
||||
}
|
||||
|
||||
qualifiedDottedName() {
|
||||
return this.qualifiedName().replace(/[$]/g, '.');
|
||||
}
|
||||
|
||||
validate() {
|
||||
const checkSuperDeclarations = () => {
|
||||
const res = {
|
||||
extends: [],
|
||||
implements: [],
|
||||
first: this.super_declarations[0],
|
||||
};
|
||||
const problems = [];
|
||||
this.super_declarations.forEach((sd) => res[sd.decl_kw.text].push(sd));
|
||||
for (let i = 1; i < res.extends.length; i++) {
|
||||
problems.push(new ParseProblem(res.extends[i].decl_kw, `Types cannot have multiple 'extends' declarations`, ProblemSeverity.Error));
|
||||
}
|
||||
for (let i = 1; i < res.implements.length; i++) {
|
||||
problems.push(new ParseProblem(res.extends[i].decl_kw, `Types cannot have multiple 'implements' declarations`, ProblemSeverity.Error));
|
||||
}
|
||||
if (res.extends.length > 0 && res.implements.length > 0 && res.first.decl_kw.text !== 'extends') {
|
||||
problems.push(new ParseProblem(res.extends[0].decl_kw, `'extends' declaration must appear before 'implements'`, ProblemSeverity.Error));
|
||||
}
|
||||
if (this.kind === 'class' && res.extends.length === 1 && res.extends[0].typelist.length > 1) {
|
||||
problems.push(new ParseProblem(res.extends[0].decl_kw, `Class types cannot extend from multiple super types`, ProblemSeverity.Error));
|
||||
}
|
||||
return problems;
|
||||
};
|
||||
const checkDuplicateFieldNames = () => {
|
||||
// get list of fields, sorted by name
|
||||
const fields = this.declarations
|
||||
.filter((d) => d.kind === 'field')
|
||||
.slice()
|
||||
.sort((a, b) => a.name.text.localeCompare(b.name.text));
|
||||
const probs = [];
|
||||
let name = '';
|
||||
fields.forEach((decl, idx, arr) => {
|
||||
const next = arr[idx + 1];
|
||||
if ((next && decl.name.text === next.name.text) || decl.name.text === name) {
|
||||
probs.push(new ParseProblem(decl.name, `Duplicate field name: '${decl.name.text}'`, ProblemSeverity.Error));
|
||||
}
|
||||
name = decl.name.text;
|
||||
});
|
||||
return probs;
|
||||
};
|
||||
let problems = [
|
||||
...ParseProblem.checkDuplicateModifiers(this.modifiers),
|
||||
...ParseProblem.checkConflictingModifiers(this.modifiers),
|
||||
...ParseProblem.checkAccessModifiers(this.modifiers, this.kind),
|
||||
...ParseProblem.checkNonKeywordIdents([this.name]),
|
||||
...ParseProblem.checkNonKeywordIdents(this.declarations.map((d) => d.name)),
|
||||
...checkDuplicateFieldNames(),
|
||||
...checkSuperDeclarations(),
|
||||
...this.declarations.reduce((probs, d) => {
|
||||
return [...probs, ...d.validate()];
|
||||
}, []),
|
||||
];
|
||||
return problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} package_name
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,*>} typemap
|
||||
*/
|
||||
validateTypes(package_name, imports, typemap) {
|
||||
const problems = [];
|
||||
const fqtypename = package_name ? `${package_name}.${this.qualifiedName()}` : this.qualifiedName();
|
||||
|
||||
/** @type {TypeIdent[]} */
|
||||
let typeidents = [];
|
||||
|
||||
// check extends
|
||||
this.super_declarations.filter(sd => sd.decl_kw.text === 'extends').forEach(sd => {
|
||||
sd.typelist.forEach(typeident => typeidents.push(typeident));
|
||||
})
|
||||
const resolved_extends = resolveTypeIdents(typeidents, package_name, imports, typemap);
|
||||
resolved_extends.forEach((rt,i) => {
|
||||
checkResolvedType(rt, typeidents[i]);
|
||||
if (this.kind === 'class' && rt.mtis.length === 1) {
|
||||
// class extend type must be a class
|
||||
if (rt.mtis[0].typeKind !== 'class') {
|
||||
problems.push(new ParseProblem(typeidents[i].tokens, `Class '${this.name.text}' cannot extend from ${rt.mtis[0].typeKind} '${rt.label}'; the specified type must be a non-final class.`, ProblemSeverity.Error));
|
||||
}
|
||||
// class extend type cannot be final
|
||||
else if (rt.mtis[0].hasModifier('final')) {
|
||||
problems.push(new ParseProblem(typeidents[i].tokens, `Class '${this.name.text}' cannot extend from final class '${rt.mtis[0].fullyDottedRawName}'.`, ProblemSeverity.Error));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// check implements
|
||||
typeidents = [];
|
||||
this.super_declarations.filter(sd => sd.decl_kw.text === 'implements').forEach(sd => {
|
||||
sd.typelist.forEach(typeident => typeidents.push(typeident));
|
||||
if (this.kind !== 'class' && this.kind !== 'enum') {
|
||||
problems.push(new ParseProblem(sd.decl_kw, `implements declarations are not permitted for ${this.kind} types`, ProblemSeverity.Error));
|
||||
}
|
||||
})
|
||||
const resolved_implements = resolveTypeIdents(typeidents, package_name, imports, typemap);
|
||||
resolved_implements.forEach((rt,i) => {
|
||||
checkResolvedType(rt, typeidents[i]);
|
||||
if (/class|enum/.test(this.kind) && rt.mtis.length === 1) {
|
||||
// class implements types must be interfaces
|
||||
if (rt.mtis[0].typeKind !== 'interface') {
|
||||
problems.push(new ParseProblem(typeidents[i].tokens, `Type '${this.name.text}' cannot implement ${rt.mtis[0].typeKind} type '${rt.mtis[0].fullyDottedRawName}'; the specified type must be an interface.`, ProblemSeverity.Error));
|
||||
}
|
||||
else if (!this.findModifier('abstract')) {
|
||||
// if the class is not abstract, it must implement all the methods in the interface
|
||||
// - we can't check this until the MTI for the class is complete
|
||||
const unimplemented_methods = rt.mtis[0].methods.filter(m => true);
|
||||
unimplemented_methods.forEach(method => {
|
||||
problems.push(new ParseProblem(typeidents[i].tokens, `Type '${this.name.text}' is not abstract and does not implement method '${method.toDeclSource()}' declared in interface '${rt.mtis[0].fullyDottedRawName}'.`, ProblemSeverity.Error));
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// check field, method-return and parameter types
|
||||
typeidents = [];
|
||||
this.declarations.forEach((d) => {
|
||||
if (d.kind !== 'constructor') {
|
||||
typeidents.push(d.type);
|
||||
}
|
||||
if (d.parameters) {
|
||||
d.parameters.forEach((p) => {
|
||||
typeidents.push(p.type);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const resolved_types = resolveTypeIdents(typeidents, fqtypename, imports, typemap);
|
||||
// warn about missing and ambiguous types
|
||||
function checkResolvedType(rt, typeident) {
|
||||
if (rt.error) {
|
||||
problems.push(new ParseProblem(typeident.tokens, rt.error, ProblemSeverity.Error));
|
||||
return;
|
||||
}
|
||||
if (rt.mtis.length === 0) {
|
||||
problems.push(new ParseProblem(typeident.tokens, `Type not found: ${rt.label}`, ProblemSeverity.Error));
|
||||
return;
|
||||
}
|
||||
if (rt.mtis.length > 1) {
|
||||
const names = rt.mtis.map(mti => mti.fullyDottedRawName).join(`' or '`);
|
||||
problems.push(new ParseProblem(typeident.tokens, `Ambiguous type: ${rt.label} - could be '${names}'.`, ProblemSeverity.Error));
|
||||
return;
|
||||
}
|
||||
rt.mtis.forEach(mti => {
|
||||
// void arrays are illegal
|
||||
if (mti.name.startsWith('void[')) {
|
||||
problems.push(new ParseProblem(typeident.tokens, `primitive void arrays are not a valid type.`, ProblemSeverity.Error));
|
||||
}
|
||||
})
|
||||
}
|
||||
resolved_types.forEach((rt,i) => {
|
||||
checkResolvedType(rt, typeidents[i]);
|
||||
|
||||
// check any type arguments
|
||||
rt.parts.filter(p => p.typeargs).forEach(p => {
|
||||
p.typeargs.forEach(typearg => {
|
||||
checkResolvedType(typearg, typeidents[i]);
|
||||
// check type arguments are not primitives (primitive arrays are ok)
|
||||
if (typearg.mtis.length === 1) {
|
||||
if (typearg.mtis[0].typeKind === 'primitive') {
|
||||
problems.push(new ParseProblem(typeidents[i].tokens, `Type arguments cannot be primitive types.`, ProblemSeverity.Error));
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
return problems;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TypeDeclaration;
|
||||
24
langserver/java/parsetypes/typeident.js
Normal file
24
langserver/java/parsetypes/typeident.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @typedef {import('./token')} Token
|
||||
* @typedef {import('./resolved-type')} ResolvedType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to represent a declared type in methods, fields, parameters and variables
|
||||
*/
|
||||
class TypeIdent {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
constructor(tokens) {
|
||||
this.tokens = tokens;
|
||||
/** @type {ResolvedType} */
|
||||
this.resolved = null;
|
||||
}
|
||||
|
||||
lastToken() {
|
||||
return this.tokens[this.tokens.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TypeIdent;
|
||||
Reference in New Issue
Block a user