mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
version 1.2 (#93)
* initial working language server * first hacky version of source parsing and type checking * first iteration of method body parser * add support for prefix/postfix inc expressions * add basic support for parsing new expressions * different attempt to parse using collapsable text ranges * fix parsing of binary operstors following a bracket expression * updated validation to use new JavaTypes module instead of MTIs * add support for array-literal expressions * fix || and && not being tokenized as operators allow float literals starting with dot * add new method body parser to use direct linear parsing * add super as an object literal * fix interface constructors check constructor type modifiers * fix assignment operator types * Fix resolving of enclosed type identifiers * add default constructor for class types with no explicit constructors * add missing constructor validator * add constructor parameters to list of resolvable types * update SourceMethod to pass name in super constructor * add Any* classes to reduce cascading errors * update method call parameter checking use isTypeAssignable instead of getParameterCompatibleTypeSignatures * tidy up isTypeAssignable allow class equivilents for primitives * add more info when methods/ctrs cannot be matched * allow interfaces to be cast to class instances * use isTypeAssignable for checking branch test expressions * allow AnyValue to be a constant value * split shift operators from bitwise operators * add support for literal numbers to be assignable to multiple primtive types * clear diagnostics when document is closed * update check for cast expression * casting only applies to qualified term not a whole expression * allow all primitive-number-type casts * add support for synchronized statement * update primitive type compatibility * allow null to be cast to any non-primitve * use better regex for string literals * allow character literals to be assigned to number types * add support for array qualifiers after a variable name * make sure any long specifier is stripped from a bigint value * improve invalid array expression message add AnyType array element to prevent cascading errors * make default a modifer keyword for interface default method support * initial support for wildcard type arguments * fix parse issue with nested generic types * allow generic types to be assigned to inherited types with compatible type arguments * allow unicode characters, $ and _ in identifiers * map primitive types to their boxed versions for class member * support assert statement * allow unicode char literals * make type parser and body parser use same tokenizer * reuse parsed tokens instead of tokenizing each method body * re-add throws as a keyword * treat default and synchronized as modifiers * add SourceInitialiser support * refactor to prepare for merging with type parsing * add support for array qualifiers in type identifiers * pass scoped type instead of method to typeIdent * update ResolvableType to use same type resolving as method body parsing * add support for post-name array qualifiers in fields and parameters * post-name array qualifiers in method decls * add type variables to SourceMethod * initial attempt to support type variable arguments in methods * specialise methods with type variables * don't require default interface methods to be implemented * make variable arity parameters an array type * tidy array constructors and fix some warnings * update isCallCompatible to handle variable arity calls * improve assert statement support * parse labels and break/continue targets * refactor new term qualifiers * add support for generic inferred-type arguments * improve modifier checks for interface types * improve reporting of unresolved type errors * fix type checking of field and method declarations * add missng strictfp modifier * refactor in preparation for parsing local types * replace Locals with scopeable MethodDeclarations to allow labels and types to be stored * initial changes to support local type declarations * update to use new set of SourceX classes * refactor to allow expressions to have a type scope * replace regex parsing with linear parsing * generate source types before parsing * fix support for resolving type variables in method declarations * fix checking of array literal compatability * report errors from unit parsing * remove local modifier validation during parse add parameter modifier checking to validation * allow trailing comma for array literals * start separating validation from parsing * add support for parsing enum values * allow uppercase 0X in hex literals * include enclosing types in identifier search * add support for parsing parameterless lambdas * ignore unresolved types in extends/implements * implement specialisation of SourceType * allow super as a member qualifier * allow empty enums * don't report missing constructors if superclass has none * update typemap declarations to use CEIType instead of JavaType * fix resolving of class type variables * fix bad imports when resolving annotations * allow null scope in findIdentifier * add support for static member imports * import types from same package * remove this qualifier from isCastExpression * add hex exponent support * parse try-with-resources * fix resolving imported enclosed types * extract expression types into separate files * extract statement types into separate files * fix type warnings * extract literals into separate files * remove Value class, add NewExpression and separate out Any classes * rename source types module * remove some parse checks that should be in verify * support token extraction in expressions * implement resolveExpression * add type cast checking * check for valid type in class member expressions * allow assigns for assignable type arguments * improve reporting of unresolved identifiers * add new array validation * validate array literals * validate array indexes * improve validation of binary operators * rename ResolvedType to ResolvedValue * improve checking of number literals * support package name as a resolved value * implement method body and ststement validation * improve method call resolving * add support for this() and super() constructor calls * remove return type for source constructors * add checks for unary operators * ensure tokens are assigned for qualified expressions * check castability using type assignments * add implicit enum methods values() and valueOf() * add basic type checking of lambda expressions * fix return type check * fix assert statement checks * improve support for ternary operators in assignments and method invocations * perform more detailed search of implemented methods * initial test of context-dependant code completion * support package, type and static field import completion * support for member expressions * use exact type signatures for locating types for completion items * add support for field and method docs * add support for docs in source types * support member completion for array types improve comment formatting * ensure Object is always last in the list of inherited types * add owning method to statements create common keyword statement class * improve code completion list add method parameters order list items by scope * add source types to list hide this and super for non-methods * fix bad member resolution at end of block fix missing method and type docs * add support for editing multiple files * allow multiple source files to be used in parsing * load and parse files at startup * add support for displaying method signatures * add single trace function with timestamps * implement shceduleReparse to reduce parsing load while typing * remove parsed type list logging * wait for reparsing before returning method signatures * resolve new object contructors * improve extraction of parameter docs * update @types/vscode * cache decoded android library in globalStoragePath * load single android library cache from local folder * android-29 library cache * allow configurable app root setting * set configurable trace logging and update section names * description updates * handle null token passed to ParseProblem * refactoring * Rename language client extension to Android * ignore unnamed type declarations * handle java file change notifications * make sure we only try and parse java files * add option to allow language server to be shutdown * simplify handling of this and class member qualifiers * relocate java-mti package into project * get main node install to install langserver dependencies * remove debugging pause * rename body-parser3 to body-parser * clean up import resolving code * remove unused field from ResolvedImport * remove validation modules that used old parser types * remove old parser files * remove redundant types and functions used by old parser * move addproblem into TokenList * remove unused ResolvedType class * validate more statements * add support for parsing and validating anonymous types * hide some method modifiers which aren't useful to show * code comments and minor improvements * fix some type warnings * improve support for completion of enum values * add type name to parameter completion labels * ignore synthetic members in completion list * use a specialised map for handling case-insenstive file uris * add basic build script * reference java-mti package from GitHub * revert @types/vscode * update initial file loading to use URIs passed from the client changes to the appSourceRoot now require an extension restart * add support for loading filtered androidx libraries for code completion * update version of java-mti * add mixpanel package * add basic analytics * fix dependency versions * fix dependency versions * set empty cache file markers * add language server debug config * add file to build script * add unqualified type members when inside a method * apply statics filter to enum values * add basic debugger analytics * include current time in startup event * add terminate reason to debugger * update changelog and readme
This commit is contained in:
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -13,7 +13,17 @@
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Language Server",
|
||||
"port": 6009,
|
||||
"cwd": "${workspaceFolder}/langserver",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Debugger Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
@@ -37,6 +47,10 @@
|
||||
{
|
||||
"name": "Extension + Debugger",
|
||||
"configurations": [ "Launch Extension", "Debugger Server" ]
|
||||
},
|
||||
{
|
||||
"name": "Debug Language Server",
|
||||
"configurations": [ "Launch Extension", "Attach to Language Server" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
# Change Log
|
||||
|
||||
### version 1.2.0
|
||||
* Java Intellisense beta.
|
||||
|
||||
### version 1.1.0
|
||||
* App launch arguments overriden in a new `amStartArgs` launch configuration property.
|
||||
* A new "attach" launch configuration allows the debugger to attach to running processes.
|
||||
|
||||
20
README.md
20
README.md
@@ -2,6 +2,9 @@
|
||||
|
||||
This is a preview version of the Android for VS Code Extension. The extension allows developers to install, launch and debug Android Apps from within the VS Code environment.
|
||||
|
||||
## What's New
|
||||
- Java Intellisense for Android is now in beta.
|
||||
|
||||
## Features
|
||||
* Line by line code stepping
|
||||
* Breakpoints
|
||||
@@ -18,12 +21,11 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
|
||||
## Limitations
|
||||
|
||||
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
|
||||
* This extension **will not build your app**.
|
||||
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug`.
|
||||
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode task to automatically build your app before launching a debug session.
|
||||
* This extension will not build your app.
|
||||
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug` or configure a VSCode Build Task to run the command (see below).
|
||||
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode Task to automatically build your app before launching a debug session.
|
||||
* Some debugger options are yet to be implemented. You cannot set conditional breakpoints and watch expressions must be simple variables.
|
||||
* If you require a must-have feature that isn't there yet, let us know on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
|
||||
* This extension does not provide any additional code completion or other editing enhancements.
|
||||
|
||||
## Extension Settings
|
||||
|
||||
@@ -149,6 +151,16 @@ Add a new task to run the build command:
|
||||
}
|
||||
```
|
||||
|
||||
## Java Intellisense
|
||||
Support for Java Intellisense is currently in beta, so any **feedback is appreciated**.
|
||||
|
||||
To use Java intellisense, make sure the option is enabled in Settings (Extensions > Android > Enable Java language support for Android)
|
||||
and press `ctrl/cmd-space` when editing a Java source file.
|
||||
|
||||
You can read more about using code-completion on the [VSCode website](https://code.visualstudio.com/docs/editor/intellisense) and how to configure code-completion to suit your Android project in the [wiki](https://github.com/adelphes/android-dev-ext/wiki).
|
||||
|
||||

|
||||
|
||||
## Expression evaluation
|
||||
|
||||
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.
|
||||
|
||||
119
extension.js
119
extension.js
@@ -1,18 +1,127 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
const path = require('path');
|
||||
const vscode = require('vscode');
|
||||
const analytics = require('./langserver/analytics');
|
||||
const package_json = require('./package.json');
|
||||
const { LanguageClient, TransportKind, } = require('vscode-languageclient');
|
||||
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||
const { openLogcatWindow } = require('./src/logcat');
|
||||
const { selectAndroidProcessID } = require('./src/process-attach');
|
||||
const { selectTargetDevice } = require('./src/utils/device');
|
||||
|
||||
// this method is called when your extension is activated
|
||||
// your extension is activated the very first time the command is executed
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
async function createLanguageClient(context) {
|
||||
// The server is implemented in node
|
||||
let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js'));
|
||||
// The debug options for the server
|
||||
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
|
||||
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
|
||||
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
/** @type {import('vscode-languageclient').ServerOptions} */
|
||||
let serverOptions = {
|
||||
run: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
},
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
options: debugOptions
|
||||
}
|
||||
};
|
||||
|
||||
const config = vscode.workspace.getConfiguration('android-dev-ext');
|
||||
const appSourceRoot = config.get('appSourceRoot', '');
|
||||
let globSearchRoot = appSourceRoot;
|
||||
if (globSearchRoot) {
|
||||
// for findFiles to work properly, the path cannot begin with slash or have any relative components
|
||||
globSearchRoot = path.normalize(appSourceRoot.replace(/(^[\\/]+)|([\\/]+$)/,''));
|
||||
if (globSearchRoot) globSearchRoot += '/';
|
||||
}
|
||||
const sourceFiles = (await vscode.workspace.findFiles(`${globSearchRoot}**/*.java`, null, 1000, null)).map(uri => uri.toString());
|
||||
|
||||
const mpids = analytics.getIDs(context);
|
||||
|
||||
// Options to control the language client
|
||||
/** @type {import('vscode-languageclient').LanguageClientOptions} */
|
||||
let clientOptions = {
|
||||
// Register the server for Java source documents
|
||||
documentSelector: [{
|
||||
scheme: 'file', language: 'java'
|
||||
}],
|
||||
initializationOptions: {
|
||||
// extensionPath points to the root of the extension (the folder where this file is)
|
||||
extensionPath: context.extensionPath,
|
||||
mpuid: mpids.uid,
|
||||
mpsid: mpids.sid,
|
||||
initialSettings: config,
|
||||
sourceFiles,
|
||||
vscodeVersion: vscode.version,
|
||||
workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()),
|
||||
},
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.java files contained in the workspace
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.java')
|
||||
},
|
||||
};
|
||||
|
||||
// Create the language client - this won't do anything until start() is called
|
||||
return new LanguageClient(
|
||||
'androidJavaLanguageServer',
|
||||
'Android',
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {LanguageClient} */
|
||||
let languageClient;
|
||||
let languageSupportEnabled = false;
|
||||
function refreshLanguageServerEnabledState() {
|
||||
if (!languageClient) {
|
||||
return;
|
||||
}
|
||||
let langSupport = vscode.workspace.getConfiguration('android-dev-ext').get('languageSupport', false);
|
||||
if (langSupport === languageSupportEnabled) {
|
||||
return;
|
||||
}
|
||||
if (langSupport) {
|
||||
if (languageClient.needsStart()) {
|
||||
languageClient.start();
|
||||
}
|
||||
languageSupportEnabled = true;
|
||||
} else {
|
||||
if (languageClient.needsStop()) {
|
||||
languageClient.stop().then(() => {
|
||||
languageSupportEnabled = false;
|
||||
refreshLanguageServerEnabledState();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
|
||||
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
|
||||
AndroidContentProvider.register(context, vscode.workspace);
|
||||
|
||||
const mpids = analytics.getIDs(context);
|
||||
analytics.init(undefined, mpids.uid, mpids.sid, package_json, { vscode_version: vscode.version });
|
||||
|
||||
createLanguageClient(context).then(client => {
|
||||
languageClient = client;
|
||||
refreshLanguageServerEnabledState();
|
||||
});
|
||||
|
||||
// The commandId parameter must match the command field in package.json
|
||||
const disposables = [
|
||||
// add the view logcat handler
|
||||
@@ -50,6 +159,12 @@ function activate(context) {
|
||||
// the debugger requires a string value to be returned
|
||||
return JSON.stringify(o);
|
||||
}),
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
// perform the refresh on the next tick to prevent spurious errors when
|
||||
// trying to shut down the language server in the middle of a change-configuration request
|
||||
process.nextTick(() => refreshLanguageServerEnabledState());
|
||||
}),
|
||||
];
|
||||
|
||||
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
|
||||
|
||||
BIN
images/java-intellisense.png
Normal file
BIN
images/java-intellisense.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
0
langserver/.library-cache/android-29.zip
Normal file
0
langserver/.library-cache/android-29.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
143
langserver/analytics.js
Normal file
143
langserver/analytics.js
Normal file
@@ -0,0 +1,143 @@
|
||||
let mp;
|
||||
/** @type {string} */
|
||||
let uid;
|
||||
/** @type {string} */
|
||||
let sid;
|
||||
/** @type {Map<string,[number,number]>} */
|
||||
const timeLabels = new Map();
|
||||
let session_start = Date.now();
|
||||
|
||||
/**
|
||||
* @param {string} [t]
|
||||
* @param {string} [u]
|
||||
* @param {string} [s]
|
||||
* @param {{name:string,version:string}} [package_json]
|
||||
* @param {*} [props]
|
||||
*/
|
||||
function init(t = '0cca95950055c6553804a46ce7e3df18', u, s, package_json, props) {
|
||||
if (mp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mp = require('mixpanel').init(t);
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
uid = u;
|
||||
sid = s;
|
||||
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
const os = require('os');
|
||||
const now = new Date();
|
||||
event(`${package_json.name}-start`, {
|
||||
extension: package_json.name,
|
||||
ext_version: package_json.version,
|
||||
arch: process.arch,
|
||||
cpus: os.cpus().length,
|
||||
mem: (os.totalmem() / 1e6)|0,
|
||||
platform: process.platform,
|
||||
node_version: process.version,
|
||||
release: os.release(),
|
||||
localtime: now.toTimeString(),
|
||||
tz: now.getTimezoneOffset(),
|
||||
...props
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @param {*} [properties]
|
||||
*/
|
||||
function event(eventName, properties) {
|
||||
if (!mp) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (uid) {
|
||||
mp.track(eventName, {
|
||||
distinct_id: uid,
|
||||
session_id: sid,
|
||||
session_length: Math.trunc((Date.now() - session_start) / 60e3),
|
||||
...properties,
|
||||
});
|
||||
} else {
|
||||
mp.track(eventName, properties);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
*/
|
||||
function time(label) {
|
||||
if (!label || timeLabels.has(label)) {
|
||||
return;
|
||||
}
|
||||
timeLabels.set(label, process.hrtime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @param {'ns'|'us'|'ms'|'s'} time_unit
|
||||
* @param {*} [additionalProps]
|
||||
*/
|
||||
function timeEnd(label, time_unit = 'ms', additionalProps = {}) {
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
const startTime = timeLabels.get(label);
|
||||
timeLabels.delete(label);
|
||||
if (!Array.isArray(startTime)) {
|
||||
return;
|
||||
}
|
||||
const elapsed = process.hrtime(startTime);
|
||||
const count = time_unit === 's' ? elapsed[0] : ((elapsed[0]*1e9) + elapsed[1]);
|
||||
const divs = {
|
||||
ns: 1, us: 1e3, ms: 1e6, s: 1
|
||||
}
|
||||
const props = {
|
||||
[`${label}-elapsed`]: Math.trunc(count / (divs[time_unit] || 1)),
|
||||
[`${label}-elapsed_unit`]: time_unit,
|
||||
...additionalProps,
|
||||
}
|
||||
event(label, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vscode').ExtensionContext} context
|
||||
*/
|
||||
function getIDs(context) {
|
||||
if (!context || !context.globalState) {
|
||||
return {
|
||||
uid: '', sid: ''
|
||||
};
|
||||
}
|
||||
let uuidv4 = () => {
|
||||
try {
|
||||
uuidv4 = require('uuid').v4;
|
||||
return uuidv4();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
let u = uid || (uid = context.globalState.get('mix-panel-id'));
|
||||
if (typeof u !== 'string' || u.length > 36) {
|
||||
u = uid = uuidv4();
|
||||
context.globalState.update('mix-panel-id', u);
|
||||
}
|
||||
let s = sid || (sid = uuidv4());
|
||||
return {
|
||||
uid: u,
|
||||
sid: s,
|
||||
}
|
||||
}
|
||||
|
||||
exports.init = init;
|
||||
exports.event = event;
|
||||
exports.time = time;
|
||||
exports.timeEnd = timeEnd;
|
||||
exports.getIDs = getIDs;
|
||||
113
langserver/build.js
Normal file
113
langserver/build.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* This is a really basic module packer. It simply loads all the modules into a
|
||||
* single object, appends a simple bootstrap require function to 'load' the modules at
|
||||
* runtime and then writes the result out to a entry module.
|
||||
*
|
||||
* - Each local module (i.e not in node_modules) must be a relative path starting with . or ..
|
||||
* - Subfolders are allowed, but only js files are included.
|
||||
*/
|
||||
|
||||
/** These are the sources (files and folders) we want to pack */
|
||||
const sources = [
|
||||
'analytics.js',
|
||||
'completions.js',
|
||||
'doc-formatter.js',
|
||||
'document.js',
|
||||
'java',
|
||||
'logging.js',
|
||||
'method-signatures.js',
|
||||
'server.js',
|
||||
'settings.js'
|
||||
]
|
||||
|
||||
/** The entry module - must have a relative path */
|
||||
const entry = './server.js';
|
||||
|
||||
const fs = require('fs');
|
||||
const modules = [];
|
||||
|
||||
while (sources.length) {
|
||||
const source = sources.shift();
|
||||
const stat = fs.statSync(source);
|
||||
if (stat.isDirectory()) {
|
||||
fs.readdirSync(source).forEach(entry => {
|
||||
sources.unshift(`${source}/${entry}`);
|
||||
})
|
||||
continue;
|
||||
}
|
||||
if (!source.endsWith('.js')) {
|
||||
console.log(`ignoring non-js file: ${source}`);
|
||||
continue;
|
||||
}
|
||||
// add an object entry of the form: 'path': (...) => { file_content }
|
||||
modules.push(`'${source}':
|
||||
(require,module,exports) => {
|
||||
${fs.readFileSync(source, 'utf8')}
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The bootstrap contains the custom require function and the call to load
|
||||
* the initial module - it's everything after the marker below
|
||||
*/
|
||||
const bootstrap = fs.readFileSync(__filename, 'utf8').split('/* bootstrap marker */').pop();
|
||||
|
||||
fs.writeFileSync(entry,
|
||||
`const data = {
|
||||
${modules.join(',\n')}
|
||||
}
|
||||
${bootstrap}
|
||||
_require('${entry}');
|
||||
`);
|
||||
|
||||
/* bootstrap marker */
|
||||
|
||||
const module_stack = [{
|
||||
path: [],
|
||||
name: '',
|
||||
}]
|
||||
|
||||
const loadedModules = new Set();
|
||||
|
||||
function _require(filename) {
|
||||
// local modules always have a relative path
|
||||
if (!filename.startsWith('.')) {
|
||||
// node_modules import
|
||||
return require(filename);
|
||||
}
|
||||
const new_path = module_stack[0].path.slice();
|
||||
let key = filename.replace(/(\.js)?$/, '.js');
|
||||
for (let m; m = key.match(/^\.\.?\//);) {
|
||||
key = key.slice(m[0].length);
|
||||
if (m[0] === '../') {
|
||||
new_path.pop();
|
||||
}
|
||||
}
|
||||
key = [...new_path, key].join('/');
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
throw new Error(`Missing module: ${key}`);
|
||||
}
|
||||
|
||||
const entry = data[key];
|
||||
|
||||
if (loadedModules.has(key)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
const path_parts = key.split(/[\\/]/);
|
||||
|
||||
module_stack.unshift({
|
||||
name: path_parts.pop(),
|
||||
path: path_parts,
|
||||
})
|
||||
const mod = {
|
||||
exports: {},
|
||||
}
|
||||
entry(_require, mod, mod.exports);
|
||||
module_stack.shift();
|
||||
|
||||
loadedModules.add(key);
|
||||
return data[key] = mod.exports;
|
||||
}
|
||||
514
langserver/completions.js
Normal file
514
langserver/completions.js
Normal file
@@ -0,0 +1,514 @@
|
||||
const { JavaType, CEIType, ArrayType, PrimitiveType } = require('java-mti');
|
||||
const { getTypeInheritanceList } = require('./java/expression-resolver');
|
||||
const { CompletionItem, CompletionItemKind } = require('vscode-languageserver');
|
||||
const { SourceType } = require('./java/source-types');
|
||||
const { indexAt } = require('./document');
|
||||
const { formatDoc } = require('./doc-formatter');
|
||||
const { trace } = require('./logging');
|
||||
const { event } = require('./analytics');
|
||||
|
||||
/**
|
||||
* Case-insensitive sort routines
|
||||
*/
|
||||
const sortBy = {
|
||||
label: (a,b) => a.label.localeCompare(b.label, undefined, {sensitivity: 'base'}),
|
||||
name: (a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}),
|
||||
}
|
||||
|
||||
/** Map Java typeKind values to vscode CompletionItemKinds */
|
||||
const TypeKindMap = {
|
||||
class: CompletionItemKind.Class,
|
||||
interface: CompletionItemKind.Interface,
|
||||
'@interface': CompletionItemKind.Interface,
|
||||
enum: CompletionItemKind.Enum,
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a list of vscode-compatible completion items for a given type.
|
||||
*
|
||||
* The type is located in typemap and the members (fields, methods) are retrieved
|
||||
* and converted to completions items.
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap Set of known types
|
||||
* @param {string} type_signature Type to provide completion items for
|
||||
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
|
||||
* @param {string[]} [typelist] optional pre-prepared type list (to save recomputing it)
|
||||
*/
|
||||
function getTypedNameCompletion(typemap, type_signature, opts, typelist) {
|
||||
let type, types, subtype_search;
|
||||
const arr_match = type_signature.match(/^\[+/);
|
||||
if (arr_match) {
|
||||
// for arrays, just create a dummy type
|
||||
types = [
|
||||
type = new ArrayType(PrimitiveType.map.V, arr_match[0].length),
|
||||
typemap.get('java/lang/Object'),
|
||||
];
|
||||
} else if (!/^L.+;/.test(type_signature)) {
|
||||
return [];
|
||||
} else {
|
||||
type = typemap.get(type_signature.slice(1,-1));
|
||||
if (!type) {
|
||||
return [];
|
||||
}
|
||||
if (!(type instanceof CEIType)) {
|
||||
return [];
|
||||
}
|
||||
// retrieve the complete list of inherited types
|
||||
types = getTypeInheritanceList(type);
|
||||
subtype_search = type.shortSignature + '$';
|
||||
}
|
||||
|
||||
|
||||
class SetOnceMap extends Map {
|
||||
set(key, value) {
|
||||
return this.has(key) ? this : super.set(key, value);
|
||||
}
|
||||
}
|
||||
const fields = new SetOnceMap(),
|
||||
methods = new SetOnceMap(),
|
||||
inner_types = new SetOnceMap(),
|
||||
enumValues = new SetOnceMap();
|
||||
|
||||
/**
|
||||
* @param {string[]} modifiers
|
||||
* @param {JavaType} t
|
||||
* @param {boolean} [synthetic]
|
||||
*/
|
||||
function shouldInclude(modifiers, t, synthetic) {
|
||||
// filter statics/instances
|
||||
if (opts.statics !== modifiers.includes('static')) return;
|
||||
// exclude synthetic entries
|
||||
if (synthetic) return;
|
||||
if (modifiers.includes('public')) return true;
|
||||
if (modifiers.includes('protected')) return true;
|
||||
// only include private items for the current type
|
||||
if (modifiers.includes('private') && t === type) return true;
|
||||
// @ts-ignore
|
||||
return t.packageName === type.packageName;
|
||||
}
|
||||
|
||||
// retrieve fields and methods
|
||||
types.forEach(t => {
|
||||
if (t instanceof SourceType && opts.statics) {
|
||||
t.enumValues.sort(sortBy.name)
|
||||
.forEach(e => enumValues.set(e.name, {e, t}))
|
||||
}
|
||||
t.fields.sort(sortBy.name)
|
||||
.filter(f => shouldInclude(f.modifiers, t, f.isSynthetic))
|
||||
.forEach(f => {
|
||||
if (f.isEnumValue) {
|
||||
enumValues.set(f.name, {e:f, t});
|
||||
} else {
|
||||
fields.set(f.name, {f, t});
|
||||
}
|
||||
});
|
||||
t.methods.sort(sortBy.name)
|
||||
.filter(m => shouldInclude(m.modifiers, t, m.isSynthetic))
|
||||
.forEach(m => methods.set(`${m.name}${m.methodSignature}`, {m, t}));
|
||||
});
|
||||
|
||||
if (opts.statics && subtype_search) {
|
||||
// retrieve inner types
|
||||
(typelist || [...typemap.keys()])
|
||||
.filter(type_signature =>
|
||||
type_signature.startsWith(subtype_search)
|
||||
// ignore inner-inner types
|
||||
&& !type_signature.slice(subtype_search.length).includes('$')
|
||||
)
|
||||
.map(type_signature => typemap.get(type_signature))
|
||||
.forEach((t,idx) => inner_types.set(t.simpleTypeName, { t }));
|
||||
}
|
||||
|
||||
return [
|
||||
// enum values
|
||||
...[...enumValues.values()].map((e,idx) => ({
|
||||
label: `${e.e.name}: ${e.t.simpleTypeName}`,
|
||||
insertText: e.e.name,
|
||||
kind: CompletionItemKind.EnumMember,
|
||||
sortText: `${idx+1000}${e.e.name}`,
|
||||
data: { type: e.t.shortSignature, fidx: e.t.fields.indexOf(e.e) },
|
||||
})),
|
||||
// fields
|
||||
...[...fields.values()].map((f,idx) => ({
|
||||
label: `${f.f.name}: ${f.f.type.simpleTypeName}`,
|
||||
insertText: f.f.name,
|
||||
kind: CompletionItemKind.Field,
|
||||
sortText: `${idx+2000}${f.f.name}`,
|
||||
data: { type: f.t.shortSignature, fidx: f.t.fields.indexOf(f.f) },
|
||||
})),
|
||||
// methods
|
||||
...[...methods.values()].map((m,idx) => ({
|
||||
label: m.m.shortlabel,
|
||||
kind: CompletionItemKind.Method,
|
||||
insertText: m.m.name,
|
||||
sortText: `${idx+3000}${m.m.name}`,
|
||||
data: { type: m.t.shortSignature, midx: m.t.methods.indexOf(m.m) },
|
||||
})),
|
||||
// types
|
||||
...[...inner_types.values()].map((it,idx) => ({
|
||||
label: it.t.simpleTypeName,
|
||||
kind: TypeKindMap[it.t.typeKind],
|
||||
sortText: `${idx+4000}${it.t.simpleTypeName}`,
|
||||
data: { type: it.shortSignature },
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of vscode-compatible completion items for a dotted identifier (package or type).
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap Set of known types
|
||||
* @param {string} dotted_name
|
||||
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
|
||||
*/
|
||||
function getFullyQualifiedDottedIdentCompletion(typemap, dotted_name, opts) {
|
||||
if (dotted_name === '') {
|
||||
// return the list of top-level package names
|
||||
return getTopLevelPackageCompletions(typemap);
|
||||
}
|
||||
// name is a fully dotted name, possibly including members and their fields
|
||||
let typelist = [...typemap.keys()];
|
||||
|
||||
const split_name = dotted_name.split('.');
|
||||
let pkgname = '';
|
||||
/** @type {JavaType} */
|
||||
let type = null, typename = '';
|
||||
for (let name_part of split_name) {
|
||||
if (type) {
|
||||
if (opts.statics && typelist.includes(`${typename}$${name_part}`)) {
|
||||
type = typemap.get(typename = `${typename}$${name_part}`);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
typename = pkgname + name_part;
|
||||
if (typelist.includes(typename)) {
|
||||
type = typemap.get(typename);
|
||||
continue;
|
||||
}
|
||||
pkgname = `${pkgname}${name_part}/`;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
return getTypedNameCompletion(typemap, type.typeSignature, opts, typelist);
|
||||
}
|
||||
|
||||
// sub-package or type
|
||||
const search_pkg = pkgname;
|
||||
return typelist.reduce((arr,typename) => {
|
||||
if (typename.startsWith(search_pkg)) {
|
||||
const m = typename.slice(search_pkg.length).match(/^(.+?)(\/|$)/);
|
||||
if (m) {
|
||||
if (m[2]) {
|
||||
// package name
|
||||
if (!arr.find(x => x.label === m[1])) {
|
||||
arr.push({
|
||||
label: m[1],
|
||||
kind: CompletionItemKind.Unit,
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// type name
|
||||
arr.push({
|
||||
label: m[1].replace(/\$/g,'.'),
|
||||
kind: CompletionItemKind.Class,
|
||||
data: { type: typename },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of completion items for top-level package names (e.g java, javax, android)
|
||||
*
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function getTopLevelPackageCompletions(typemap) {
|
||||
const pkgs = [...typemap.keys()].reduce((set, short_type_signature) => {
|
||||
// the root package is the first part of the short type signature (up to the first /)
|
||||
const m = short_type_signature.match(/(.+?)\//);
|
||||
m && set.add(m[1]);
|
||||
return set;
|
||||
}, new Set());
|
||||
|
||||
const items = [...pkgs].filter(x => x)
|
||||
.sort()
|
||||
.map(package_ident => ({
|
||||
label: package_ident,
|
||||
kind: CompletionItemKind.Unit,
|
||||
sortText: package_ident,
|
||||
}));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {string} pkg
|
||||
*/
|
||||
function getPackageCompletion(typemap, pkg) {
|
||||
if (pkg === '') {
|
||||
return getTopLevelPackageCompletions(typemap);
|
||||
}
|
||||
// sub-package
|
||||
const search_pkg = pkg + '/';
|
||||
const pkgs = [...typemap.keys()].reduce((arr,typename) => {
|
||||
if (typename.startsWith(search_pkg)) {
|
||||
const m = typename.slice(search_pkg.length).match(/^(.+?)\//);
|
||||
if (m) arr.add(m[1]);
|
||||
}
|
||||
return arr;
|
||||
}, new Set());
|
||||
|
||||
return [...pkgs].filter(x => x).sort().map(pkg => ({
|
||||
label: pkg,
|
||||
kind: CompletionItemKind.Unit,
|
||||
data: null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Cache of completion items for fixed values, keywords and Android library types */
|
||||
let defaultCompletionTypes = null;
|
||||
|
||||
/** @type {Map<string,CEIType>} */
|
||||
let lastCompletionTypeMap = null;
|
||||
|
||||
let completionRequestCount = 0;
|
||||
|
||||
function initDefaultCompletionTypes(lib) {
|
||||
defaultCompletionTypes = {
|
||||
instances: 'this super'.split(' ').map(t => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Value,
|
||||
sortText: t
|
||||
})),
|
||||
// primitive types
|
||||
primitiveTypes:'boolean byte char double float int long short void'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Keyword,
|
||||
sortText: t,
|
||||
})),
|
||||
// modifiers
|
||||
modifiers: 'public private protected static final abstract volatile native transient strictfp synchronized'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Keyword,
|
||||
sortText: t,
|
||||
})),
|
||||
// literals
|
||||
literals: 'false true null'.split(' ').map((t) => ({
|
||||
label: t,
|
||||
kind: CompletionItemKind.Value,
|
||||
sortText: t
|
||||
})),
|
||||
// type names
|
||||
types: [...lib.values()].map(
|
||||
t =>
|
||||
/** @type {CompletionItem} */
|
||||
({
|
||||
label: t.dottedTypeName,
|
||||
kind: TypeKindMap[t.typeKind],
|
||||
data: { type: t.shortSignature },
|
||||
sortText: t.dottedTypeName,
|
||||
})
|
||||
).sort(sortBy.label),
|
||||
// package names
|
||||
packageNames: getTopLevelPackageCompletions(lib),
|
||||
}
|
||||
}
|
||||
|
||||
function clearDefaultCompletionEntries() {
|
||||
defaultCompletionTypes = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the VSCode completion item request.
|
||||
*
|
||||
* @param {import('vscode-languageserver').CompletionParams} params
|
||||
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
*/
|
||||
async function getCompletionItems(params, liveParsers, androidLibrary) {
|
||||
trace('getCompletionItems');
|
||||
|
||||
if (!params || !params.textDocument || !params.textDocument.uri) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let dct = defaultCompletionTypes;
|
||||
if (!defaultCompletionTypes) {
|
||||
initDefaultCompletionTypes(androidLibrary);
|
||||
dct = defaultCompletionTypes || {};
|
||||
}
|
||||
|
||||
// wait for the Android library to load (in case we receive an early request)
|
||||
if (androidLibrary instanceof Promise) {
|
||||
androidLibrary = await androidLibrary;
|
||||
}
|
||||
|
||||
// retrieve the parsed source corresponding to the request URI
|
||||
const docinfo = liveParsers.get(params.textDocument.uri);
|
||||
if (!docinfo || !docinfo.parsed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// wait for the user to stop typing
|
||||
const preversion = docinfo.version;
|
||||
await docinfo.reparseWaiter;
|
||||
if (docinfo.version !== preversion) {
|
||||
// if the file content has changed since this request wss made, ignore it
|
||||
trace('content changed - ignoring completion items')
|
||||
/** @type {import('vscode-languageserver').CompletionList} */
|
||||
return {
|
||||
isIncomplete: true,
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
|
||||
completionRequestCount += 1;
|
||||
if ((completionRequestCount === 1) || (completionRequestCount === 5) || ((completionRequestCount % 25) === 0)) {
|
||||
event('completion-requests', { comp_req_count: completionRequestCount });
|
||||
}
|
||||
|
||||
let parsed = docinfo.parsed;
|
||||
|
||||
// save the typemap associated with this parsed state - we use this when resolving
|
||||
// the documentation later
|
||||
lastCompletionTypeMap = (parsed && parsed.typemap) || androidLibrary;
|
||||
|
||||
let locals = [],
|
||||
modifiers = dct.modifiers,
|
||||
type_members = [],
|
||||
sourceTypes = [];
|
||||
|
||||
if (parsed.unit) {
|
||||
const char_index = indexAt(params.position, parsed.content);
|
||||
const options = parsed.unit.getCompletionOptionsAt(char_index);
|
||||
|
||||
if (options.loc) {
|
||||
if (/^pkgname:/.test(options.loc.key)) {
|
||||
return getPackageCompletion(parsed.typemap, options.loc.key.split(':').pop());
|
||||
}
|
||||
if (/^fqdi:/.test(options.loc.key)) {
|
||||
// fully-qualified dotted identifier
|
||||
return getFullyQualifiedDottedIdentCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
|
||||
}
|
||||
if (/^fqs:/.test(options.loc.key)) {
|
||||
// fully-qualified static expression
|
||||
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
|
||||
}
|
||||
if (/^fqi:/.test(options.loc.key)) {
|
||||
// fully-qualified instance expression
|
||||
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: false });
|
||||
}
|
||||
}
|
||||
|
||||
// if this token is inside a method, include the parameters and this/super
|
||||
if (options.method) {
|
||||
locals = options.method.parameters
|
||||
.sort(sortBy.name)
|
||||
.map(p => ({
|
||||
label: `${p.name}: ${p.type.simpleTypeName}`,
|
||||
insertText: p.name,
|
||||
kind: CompletionItemKind.Variable,
|
||||
sortText: p.name,
|
||||
}));
|
||||
|
||||
// if this is not a static method, include this/super
|
||||
if (!options.method.modifiers.includes('static')) {
|
||||
locals.push(...dct.instances);
|
||||
}
|
||||
|
||||
type_members = getTypedNameCompletion(
|
||||
parsed.typemap,
|
||||
options.method.owner.typeSignature,
|
||||
{ statics: !!options.method.modifierTokens.find(m => m.value === 'static') }
|
||||
);
|
||||
|
||||
// if we're inside a method, don't show the modifiers
|
||||
modifiers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// add types currently parsed from the source files
|
||||
liveParsers.forEach(doc => {
|
||||
if (!doc.parsed) {
|
||||
return;
|
||||
}
|
||||
doc.parsed.unit.types.forEach(
|
||||
t => sourceTypes.push({
|
||||
label: t.dottedTypeName,
|
||||
kind: TypeKindMap[t.typeKind],
|
||||
data: { type:t.shortSignature },
|
||||
sortText: t.dottedTypeName,
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
// exclude dotted (inner) types because they result in useless
|
||||
// matches in the intellisense filter when . is pressed
|
||||
const types = [
|
||||
...dct.types,
|
||||
...sourceTypes,
|
||||
].filter(x => !x.label.includes('.'))
|
||||
.sort(sortBy.label)
|
||||
|
||||
return [
|
||||
...locals,
|
||||
...type_members,
|
||||
...dct.primitiveTypes,
|
||||
...dct.literals,
|
||||
...modifiers,
|
||||
...types,
|
||||
...dct.packageNames,
|
||||
].map((x,idx) => {
|
||||
// to force the order above, reset sortText for each item based upon a fixed-length number
|
||||
x.sortText = `${1000+idx}`;
|
||||
return x;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the detail and documentation for the specified item
|
||||
*
|
||||
* @param {CompletionItem} item
|
||||
*/
|
||||
function resolveCompletionItem(item) {
|
||||
item.detail = item.documentation = '';
|
||||
if (!lastCompletionTypeMap) {
|
||||
return item;
|
||||
}
|
||||
if (!item.data || typeof item.data !== 'object') {
|
||||
return item;
|
||||
}
|
||||
const type = lastCompletionTypeMap.get(item.data.type);
|
||||
const field = type && type.fields[item.data.fidx];
|
||||
const method = type && type.methods[item.data.midx];
|
||||
if (!type) {
|
||||
return item;
|
||||
}
|
||||
let detail, documentation, header;
|
||||
if (field) {
|
||||
detail = field.label;
|
||||
documentation = field.docs;
|
||||
header = `${field.type.simpleTypeName} **${field.name}**`;
|
||||
} else if (method) {
|
||||
detail = `${method.modifiers.filter(m => !/abstract|transient|native/.test(m)).join(' ')} ${type.simpleTypeName}.${method.name}`;
|
||||
documentation = method.docs;
|
||||
header = method.shortlabel.replace(/^\w+/, x => `**${x}**`).replace(/^(.+?)\s*:\s*(.+)/, (_,a,b) => `${b} ${a}`);
|
||||
} else {
|
||||
detail = type.fullyDottedRawName,
|
||||
documentation = type.docs,
|
||||
header = `${type.typeKind} **${type.dottedTypeName}**`;
|
||||
}
|
||||
item.detail = detail || '';
|
||||
item.documentation = formatDoc(header, documentation);
|
||||
return item;
|
||||
}
|
||||
|
||||
exports.getCompletionItems = getCompletionItems;
|
||||
exports.resolveCompletionItem = resolveCompletionItem;
|
||||
exports.clearDefaultCompletionEntries = clearDefaultCompletionEntries;
|
||||
35
langserver/doc-formatter.js
Normal file
35
langserver/doc-formatter.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Convert JavaDoc content to markdown used by vscode.
|
||||
*
|
||||
* This is a *very* rough conversion, simply looking for HTML tags and replacing them
|
||||
* with relevant markdown characters.
|
||||
* It is neither complete, nor perfect.
|
||||
*
|
||||
* @param {string} header
|
||||
* @param {string} documentation
|
||||
* @returns {import('vscode-languageserver').MarkupContent}
|
||||
*/
|
||||
function formatDoc(header, documentation) {
|
||||
return {
|
||||
kind: 'markdown',
|
||||
value: `${header ? header + '\n\n' : ''}${
|
||||
(documentation || '')
|
||||
.replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '')
|
||||
.replace(/(\n[ \t]*@[a-z]+)|(<p(?: .*)?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => {
|
||||
return prm ? ` ${prm}`
|
||||
: p ? '\n\n'
|
||||
: i ? '*'
|
||||
: b ? '**'
|
||||
: tt ? '`'
|
||||
: c ? '\n```'
|
||||
: lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``)
|
||||
: li ? '\n- '
|
||||
: a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`)
|
||||
: h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} `
|
||||
: '';
|
||||
})
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
exports.formatDoc = formatDoc;
|
||||
283
langserver/document.js
Normal file
283
langserver/document.js
Normal file
@@ -0,0 +1,283 @@
|
||||
const { CEIType } = require('java-mti');
|
||||
const ParseProblem = require('./java/parsetypes/parse-problem');
|
||||
const { parse } = require('./java/body-parser');
|
||||
const { SourceUnit } = require('./java/source-types');
|
||||
const { parseMethodBodies } = require('./java/validater');
|
||||
const { time, timeEnd, trace } = require('./logging');
|
||||
|
||||
/**
|
||||
* Marker to prevent early parsing of source files before we've completed our
|
||||
* initial source file load (we cannot accurately parse individual files until we
|
||||
* know what all the types are - hence the need to perform a first parse of all the source files).
|
||||
*
|
||||
* While we are waiting for the first parse to complete, individual files-to-parse are added
|
||||
* to this set. Once the first scan and parse is done, these are reparsed and
|
||||
* first_parse_waiting is set to `null`.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
let first_parse_waiting = new Set();
|
||||
|
||||
/**
|
||||
* Convert a line,character position to an absolute character offset
|
||||
*
|
||||
* @param {{line:number,character:number}} pos
|
||||
* @param {string} content
|
||||
*/
|
||||
function indexAt(pos, content) {
|
||||
let idx = 0;
|
||||
for (let i = 0; i < pos.line; i++) {
|
||||
idx = content.indexOf('\n', idx) + 1;
|
||||
if (idx === 0) {
|
||||
return content.length;
|
||||
}
|
||||
}
|
||||
return Math.min(idx + pos.character, content.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an absolute character offset to a line,character position
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {string} content
|
||||
*/
|
||||
function positionAt(index, content) {
|
||||
let line = 0,
|
||||
last_nl_idx = 0,
|
||||
character = 0;
|
||||
if (index <= 0) return { line, character };
|
||||
for (let idx = 0; ;) {
|
||||
idx = content.indexOf('\n', idx) + 1;
|
||||
if (idx === 0 || idx > index) {
|
||||
if (idx === 0) index = content.length;
|
||||
character = index - last_nl_idx;
|
||||
return { line, character };
|
||||
}
|
||||
last_nl_idx = idx;
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialised Map to allow for case-insensitive fileURIs on Windows.
|
||||
*
|
||||
* For cs-filesystems, this should work as a normal map.
|
||||
* For ci-filesystems, if a file URI case changes, it should be picked up
|
||||
* by the lowercase map
|
||||
*/
|
||||
class FileURIMap extends Map {
|
||||
lowerMap = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
get(key) {
|
||||
return super.get(key) || this.lowerMap.get(key.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
has(key) {
|
||||
return super.has(key) || this.lowerMap.has(key.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
this.lowerMap.set(key.toLowerCase(), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
delete(key) {
|
||||
this.lowerMap.delete(key.toLowerCase());
|
||||
return super.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
this.lowerMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for storing data about Java source files
|
||||
*/
|
||||
class JavaDocInfo {
|
||||
/**
|
||||
* @param {string} uri the file URI
|
||||
* @param {string} content the full file content
|
||||
* @param {number} version revision number for edited files (each edit increments the version)
|
||||
*/
|
||||
constructor(uri, content, version) {
|
||||
this.uri = uri;
|
||||
this.content = content;
|
||||
this.version = version;
|
||||
/**
|
||||
* The result of the Java parse
|
||||
* @type {ParsedInfo}
|
||||
*/
|
||||
this.parsed = null;
|
||||
|
||||
/**
|
||||
* Promise linked to a timer which resolves a short time after the user stops typing
|
||||
* - This is used to prevent constant reparsing while the user is typing in the document
|
||||
* @type {Promise}
|
||||
*/
|
||||
this.reparseWaiter = Promise.resolve();
|
||||
|
||||
/** @type {{ resolve: () => void, timer: * }} */
|
||||
this.waitInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this document for reparsing.
|
||||
*
|
||||
* To prevent redundant parsing while typing, a small delay is required
|
||||
* before the reparse happens.
|
||||
* When a key is pressed, `scheduleReparse()` starts a timer. If more
|
||||
* keys are typed before the timer expires, the timer is restarted.
|
||||
* Once typing pauses, the timer expires and the content reparsed.
|
||||
*
|
||||
* A `reparseWaiter` promise is used to delay actions like completion items
|
||||
* retrieval and method signature resolving until the reparse is complete.
|
||||
*
|
||||
* @param {Map<string,JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
*/
|
||||
scheduleReparse(liveParsers, androidLibrary) {
|
||||
const createWaitTimer = () => {
|
||||
return setTimeout(() => {
|
||||
// reparse the content, resolve the reparseWaiter promise
|
||||
// and reset the fields
|
||||
reparse([this.uri], liveParsers, androidLibrary, { includeMethods: true });
|
||||
this.waitInfo.resolve();
|
||||
this.waitInfo = null;
|
||||
}, 250);
|
||||
}
|
||||
if (this.waitInfo) {
|
||||
// we already have a promise pending - just restart the timer
|
||||
trace('restart timer');
|
||||
clearTimeout(this.waitInfo.timer);
|
||||
this.waitInfo.timer = createWaitTimer();
|
||||
return;
|
||||
}
|
||||
// create a new pending promise and start the timer
|
||||
trace('start timer');
|
||||
this.waitInfo = {
|
||||
resolve: null,
|
||||
timer: createWaitTimer(),
|
||||
}
|
||||
this.reparseWaiter = new Promise(resolve => this.waitInfo.resolve = resolve);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from parsing a Java file
|
||||
*/
|
||||
class ParsedInfo {
|
||||
/**
|
||||
* @param {string} uri the file URI
|
||||
* @param {string} content the full file content
|
||||
* @param {number} version the version this parse applies to
|
||||
* @param {Map<string,CEIType>} typemap the set of known types
|
||||
* @param {SourceUnit} unit the parsed unit
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
constructor(uri, content, version, typemap, unit, problems) {
|
||||
this.uri = uri;
|
||||
this.content = content;
|
||||
this.version = version;
|
||||
this.typemap = typemap;
|
||||
this.unit = unit;
|
||||
this.problems = problems;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} uris
|
||||
* @param {Map<string, JavaDocInfo>} liveParsers
|
||||
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
|
||||
* @param {{includeMethods: boolean, first_parse?: boolean}} [opts]
|
||||
*/
|
||||
function reparse(uris, liveParsers, androidLibrary, opts) {
|
||||
trace(`reparse`);
|
||||
if (!Array.isArray(uris)) {
|
||||
return;
|
||||
}
|
||||
if (first_parse_waiting) {
|
||||
if (!opts || !opts.first_parse) {
|
||||
// we are waiting for the first parse to complete - add this file to the list
|
||||
uris.forEach(uri => first_parse_waiting.add(uri));
|
||||
trace('waiting for first parse')
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (androidLibrary instanceof Promise) {
|
||||
// reparse after the library has finished loading
|
||||
androidLibrary.then(lib => reparse(uris, liveParsers, lib, opts));
|
||||
return;
|
||||
}
|
||||
|
||||
const cached_units = [], parsers = [];
|
||||
for (let docinfo of liveParsers.values()) {
|
||||
if (uris.includes(docinfo.uri)) {
|
||||
// make a copy of the content + version in case the source file is edited while we're parsing
|
||||
parsers.push({uri: docinfo.uri, content: docinfo.content, version: docinfo.version});
|
||||
} else if (docinfo.parsed) {
|
||||
cached_units.push(docinfo.parsed.unit);
|
||||
}
|
||||
}
|
||||
|
||||
// Each parse uses a unique typemap, initialised from the android library
|
||||
const typemap = new Map(androidLibrary);
|
||||
|
||||
// perform the parse
|
||||
const units = parse(parsers, cached_units, typemap);
|
||||
|
||||
// create new ParsedInfo instances for each of the parsed units
|
||||
units.forEach(unit => {
|
||||
const parser = parsers.find(p => p.uri === unit.uri);
|
||||
if (!parser) return;
|
||||
const doc = liveParsers.get(unit.uri);
|
||||
if (!doc) return;
|
||||
doc.parsed = new ParsedInfo(doc.uri, parser.content, parser.version, typemap, unit, []);
|
||||
});
|
||||
|
||||
let method_body_uris = [];
|
||||
if (first_parse_waiting) {
|
||||
// this is the first parse - parse the bodies of any waiting URIs and
|
||||
// set first_parse_waiting to null
|
||||
method_body_uris = [...first_parse_waiting];
|
||||
first_parse_waiting = null;
|
||||
}
|
||||
|
||||
if (opts && opts.includeMethods) {
|
||||
method_body_uris = uris;
|
||||
}
|
||||
|
||||
if (method_body_uris.length) {
|
||||
time('parse-methods');
|
||||
method_body_uris.forEach(uri => {
|
||||
const doc = liveParsers.get(uri);
|
||||
if (!doc || !doc.parsed) {
|
||||
return;
|
||||
}
|
||||
parseMethodBodies(doc.parsed.unit, typemap);
|
||||
})
|
||||
timeEnd('parse-methods');
|
||||
}
|
||||
}
|
||||
|
||||
exports.indexAt = indexAt;
|
||||
exports.positionAt = positionAt;
|
||||
exports.FileURIMap = FileURIMap;
|
||||
exports.JavaDocInfo = JavaDocInfo;
|
||||
exports.ParsedInfo = ParsedInfo;
|
||||
exports.reparse = reparse;
|
||||
175
langserver/java/TokenList.js
Normal file
175
langserver/java/TokenList.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
*/
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TokenList} tokens
|
||||
* @param {ParseProblem} problem
|
||||
*/
|
||||
function addproblem(tokens, problem) {
|
||||
tokens.problems.push(problem);
|
||||
}
|
||||
|
||||
class TokenList {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
constructor(tokens) {
|
||||
this.tokens = tokens;
|
||||
this.idx = -1;
|
||||
/** @type {Token} */
|
||||
this.current = null;
|
||||
this.inc();
|
||||
/** @type {ParseProblem[]} */
|
||||
this.problems = [];
|
||||
this.marks = [];
|
||||
this.last_mlc = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns and consumes the current token
|
||||
*/
|
||||
consume() {
|
||||
const tok = this.current;
|
||||
this.inc();
|
||||
return tok;
|
||||
}
|
||||
|
||||
inc() {
|
||||
for (; ;) {
|
||||
this.current = this.tokens[this.idx += 1];
|
||||
if (!this.current || this.current.kind !== 'wsc') {
|
||||
return this.current;
|
||||
}
|
||||
const wsc = this.current.value;
|
||||
if (wsc.startsWith('/*')) {
|
||||
this.last_mlc = wsc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearMLC() {
|
||||
this.last_mlc = '';
|
||||
}
|
||||
|
||||
getLastMLC() {
|
||||
const s = this.last_mlc;
|
||||
this.last_mlc = '';
|
||||
return s;
|
||||
}
|
||||
|
||||
mark() {
|
||||
this.marks.unshift(this.idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of tokens from the last mark() point, trimming any trailing whitespace tokens
|
||||
*/
|
||||
markEnd() {
|
||||
let i = this.idx;
|
||||
while (this.tokens[--i].kind === 'wsc') { }
|
||||
const range = [this.marks.shift(), i + 1];
|
||||
if (range[1] <= range[0]) {
|
||||
range[1] = range[0] + 1;
|
||||
}
|
||||
return this.tokens.slice(range[0], range[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token lookahead. The current token is unaffected by this method.
|
||||
* @param {number} n number of tokens to look ahead
|
||||
*/
|
||||
peek(n) {
|
||||
let token, idx = this.idx;
|
||||
while (--n >= 0) {
|
||||
for (; ;) {
|
||||
token = this.tokens[idx += 1];
|
||||
if (!token || token.kind !== 'wsc') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified kind, returns and consumes it
|
||||
* @param {string} kind
|
||||
*/
|
||||
getIfKind(kind) {
|
||||
const token = this.current;
|
||||
if (token && token.kind === kind) {
|
||||
this.inc();
|
||||
return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified value, returns and consumes it
|
||||
* @param {string} value
|
||||
*/
|
||||
getIfValue(value) {
|
||||
const token = this.current;
|
||||
if (token && token.value === value) {
|
||||
this.inc();
|
||||
return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified value and consumes it
|
||||
* @param {string} value
|
||||
*/
|
||||
isValue(value) {
|
||||
return this.getIfValue(value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified kind and consumes it
|
||||
* @param {string} kind
|
||||
*/
|
||||
isKind(kind) {
|
||||
return this.getIfKind(kind) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current token matches the specified value and consumes it or reports an error
|
||||
* @param {string} value
|
||||
*/
|
||||
expectValue(value) {
|
||||
if (this.isValue(value)) {
|
||||
return true;
|
||||
}
|
||||
const token = this.current || this.tokens[this.tokens.length - 1];
|
||||
addproblem(this, ParseProblem.Error(token, `${value} expected`));
|
||||
return false;
|
||||
}
|
||||
|
||||
get previous() {
|
||||
for (let idx = this.idx - 1; idx >= 0; idx--) {
|
||||
if (idx <= 0) {
|
||||
return this.tokens[0];
|
||||
}
|
||||
if (this.tokens[idx].kind !== 'wsc') {
|
||||
return this.tokens[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} delete_count
|
||||
* @param {...Token} insert
|
||||
*/
|
||||
splice(start, delete_count, ...insert) {
|
||||
this.tokens.splice(start, delete_count, ...insert);
|
||||
this.current = this.tokens[this.idx];
|
||||
}
|
||||
}
|
||||
|
||||
exports.TokenList = TokenList;
|
||||
exports.addproblem = addproblem;
|
||||
172
langserver/java/anys.js
Normal file
172
langserver/java/anys.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const { JavaType, Method } = require('java-mti');
|
||||
const { Expression } = require('./expressiontypes/Expression');
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom type designed to be used where a type is missing or unresolved.
|
||||
*
|
||||
* AnyType should be fully assign/cast/type-compatible with any other type
|
||||
*/
|
||||
class AnyType extends JavaType {
|
||||
/**
|
||||
*
|
||||
* @param {String} label
|
||||
*/
|
||||
constructor(label) {
|
||||
super("class", [], '');
|
||||
super.simpleTypeName = label || '<unknown type>';
|
||||
}
|
||||
|
||||
static Instance = new AnyType('');
|
||||
|
||||
get rawTypeSignature() {
|
||||
return 'U';
|
||||
}
|
||||
|
||||
get typeSignature() {
|
||||
return 'U';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom method designed to be compatible with
|
||||
* any arguments in method call
|
||||
*/
|
||||
class AnyMethod extends Method {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(null, name, [], '');
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom expression designed to be compatiable with
|
||||
* any variable or operator
|
||||
*/
|
||||
class AnyValue extends Expression {
|
||||
/**
|
||||
*
|
||||
* @param {String} label
|
||||
*/
|
||||
constructor(label) {
|
||||
super();
|
||||
this.label = label;
|
||||
this.type = AnyType.Instance;
|
||||
}
|
||||
|
||||
resolveExpression() {
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent a method identifier
|
||||
*
|
||||
* e.g `"".length`
|
||||
*/
|
||||
class MethodType {
|
||||
/**
|
||||
* @param {Method[]} methods
|
||||
*/
|
||||
constructor(methods) {
|
||||
this.methods = methods;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent a lambda expression
|
||||
*
|
||||
* eg. `() => null`
|
||||
*/
|
||||
class LambdaType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType[]} param_types
|
||||
* @param {ResolvedValue} return_type
|
||||
*/
|
||||
constructor(param_types, return_type) {
|
||||
this.param_types = param_types;
|
||||
this.return_type = return_type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent type name expressions
|
||||
*
|
||||
* eg. `x instanceof String`
|
||||
*/
|
||||
class TypeIdentType {
|
||||
/**
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent package name expressions
|
||||
*
|
||||
* eg. `java`
|
||||
*/
|
||||
class PackageNameType {
|
||||
/**
|
||||
* @param {string} package_name
|
||||
*/
|
||||
constructor(package_name) {
|
||||
this.package_name = package_name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Custom type used to represent an array literal
|
||||
*
|
||||
* eg. `new int[] { 1,2,3 }`
|
||||
*/
|
||||
class ArrayValueType {
|
||||
/**
|
||||
* @param {{tokens:Token[], value: ResolvedValue}[]} elements
|
||||
*/
|
||||
constructor(elements) {
|
||||
this.elements = elements;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom type used to represent the types of a
|
||||
* expression that can return multiple distinct types
|
||||
*
|
||||
* eg. `x == null ? 0 : 'c'`
|
||||
*/
|
||||
class MultiValueType {
|
||||
/**
|
||||
* @param {ResolvedValue[]} types
|
||||
*/
|
||||
constructor(...types) {
|
||||
this.types = types;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./expressiontypes/literals/Number').NumberLiteral} NumberLiteral
|
||||
* @typedef {JavaType|MethodType|LambdaType|ArrayValueType|TypeIdentType|PackageNameType|MultiValueType|NumberLiteral} ResolvedValue
|
||||
**/
|
||||
|
||||
exports.AnyMethod = AnyMethod;
|
||||
exports.AnyType = AnyType;
|
||||
exports.AnyValue = AnyValue;
|
||||
exports.ArrayValueType = ArrayValueType;
|
||||
exports.LambdaType = LambdaType;
|
||||
exports.MethodType = MethodType;
|
||||
exports.MultiValueType = MultiValueType;
|
||||
exports.PackageNameType = PackageNameType;
|
||||
exports.TypeIdentType = TypeIdentType;
|
||||
1934
langserver/java/body-parser.js
Normal file
1934
langserver/java/body-parser.js
Normal file
File diff suppressed because it is too large
Load Diff
138
langserver/java/body-types.js
Normal file
138
langserver/java/body-types.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @typedef {import('./expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('./anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { JavaType, CEIType, ArrayType, Method } = require('java-mti');
|
||||
const { Token } = require('./tokenizer');
|
||||
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('./anys');
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
|
||||
class ResolvedIdent {
|
||||
/**
|
||||
* @param {string|Token} ident
|
||||
* @param {Expression[]} variables
|
||||
* @param {Method[]} methods
|
||||
* @param {JavaType[]} types
|
||||
* @param {string} package_name
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
constructor(ident, variables = [], methods = [], types = [], package_name = '', tokens = []) {
|
||||
this.source = ident instanceof Token ? ident.value : ident;
|
||||
this.variables = variables;
|
||||
this.methods = methods;
|
||||
this.types = types;
|
||||
this.package_name = package_name;
|
||||
this.tokens = ident instanceof Token ? [ident] : tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
if (this.variables[0]) {
|
||||
return this.variables[0].resolveExpression(ri);
|
||||
}
|
||||
if (this.methods[0]) {
|
||||
return new MethodType(this.methods);
|
||||
}
|
||||
if (this.types[0]) {
|
||||
return new TypeIdentType(this.types[0]);
|
||||
}
|
||||
if (this.package_name) {
|
||||
return new PackageNameType(this.package_name);
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.tokens, `Unresolved identifier: ${this.source}`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
class Local {
|
||||
/**
|
||||
* @param {Token[]} modifiers
|
||||
* @param {string} name
|
||||
* @param {Token} decltoken
|
||||
* @param {import('./source-types').SourceTypeIdent} typeIdent
|
||||
* @param {number} postnamearrdims
|
||||
* @param {ResolvedIdent} init
|
||||
*/
|
||||
constructor(modifiers, name, decltoken, typeIdent, postnamearrdims, init) {
|
||||
this.finalToken = modifiers.find(m => m.source === 'final') || null;
|
||||
this.name = name;
|
||||
this.decltoken = decltoken;
|
||||
if (postnamearrdims > 0) {
|
||||
typeIdent.resolved = new ArrayType(typeIdent.resolved, postnamearrdims);
|
||||
}
|
||||
this.typeIdent = typeIdent;
|
||||
this.init = init;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.typeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class Label {
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
this.name_token = token;
|
||||
}
|
||||
}
|
||||
|
||||
class MethodDeclarations {
|
||||
/** @type {Local[]} */
|
||||
locals = [];
|
||||
/** @type {Label[]} */
|
||||
labels = [];
|
||||
/** @type {import('./source-types').SourceType[]} */
|
||||
types = [];
|
||||
|
||||
_scopeStack = [];
|
||||
|
||||
pushScope() {
|
||||
this._scopeStack.push([this.locals, this.labels, this.types]);
|
||||
this.locals = this.locals.slice();
|
||||
this.labels = this.labels.slice();
|
||||
this.types = this.types.slice();
|
||||
}
|
||||
|
||||
popScope() {
|
||||
const prev = {
|
||||
locals: this.locals,
|
||||
labels: this.labels,
|
||||
types: this.types,
|
||||
};
|
||||
([this.locals, this.labels, this.types] = this._scopeStack.pop());
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveInfo {
|
||||
/**
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {*[]} problems
|
||||
*/
|
||||
constructor(typemap, problems) {
|
||||
this.typemap = typemap;
|
||||
this.problems = problems;
|
||||
}
|
||||
}
|
||||
|
||||
class ValidateInfo extends ResolveInfo {
|
||||
constructor(typemap, problems, method) {
|
||||
super(typemap, problems);
|
||||
this.method = method;
|
||||
/** @type {('if'|'else'|'for'|'while'|'do'|'switch'|'try'|'synchronized')[]} */
|
||||
this.statementStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
exports.Label = Label;
|
||||
exports.Local = Local;
|
||||
exports.MethodDeclarations = MethodDeclarations;
|
||||
exports.ResolvedIdent = ResolvedIdent;
|
||||
exports.ResolveInfo = ResolveInfo;
|
||||
exports.ValidateInfo = ValidateInfo;
|
||||
393
langserver/java/expression-resolver.js
Normal file
393
langserver/java/expression-resolver.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* @typedef {import('./tokenizer').Token} Token
|
||||
* @typedef {import('./anys').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
|
||||
*/
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
const { TypeVariable, JavaType, PrimitiveType, NullType, ArrayType, CEIType, WildcardType, TypeVariableType, InferredTypeArgument } = require('java-mti');
|
||||
const { AnyType, ArrayValueType, LambdaType, MultiValueType } = require('./anys');
|
||||
const { ResolveInfo } = require('./body-types');
|
||||
const { NumberLiteral } = require('./expressiontypes/literals/Number');
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {ResolvedIdent} expression
|
||||
* @param {JavaType} assign_type
|
||||
*/
|
||||
function checkAssignment(ri, assign_type, expression) {
|
||||
const value = expression.resolveExpression(ri);
|
||||
checkTypeAssignable(assign_type, value, () => expression.tokens, ri.problems);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {ResolvedValue} value
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkTypeAssignable(variable_type, value, tokens, problems) {
|
||||
if (value instanceof NumberLiteral) {
|
||||
if (!value.isCompatibleWith(variable_type)) {
|
||||
incompatibleTypesError(variable_type, value.type, () => value.tokens(), problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value instanceof MultiValueType) {
|
||||
value.types.forEach(t => checkTypeAssignable(variable_type, t, tokens, problems));
|
||||
return;
|
||||
}
|
||||
if (value instanceof ArrayValueType) {
|
||||
checkArrayLiteral(variable_type, value, tokens, problems);
|
||||
return;
|
||||
}
|
||||
if (value instanceof LambdaType) {
|
||||
checkLambdaAssignable(variable_type, value, tokens, problems);
|
||||
return;
|
||||
}
|
||||
if (value instanceof JavaType) {
|
||||
if (!isTypeAssignable(variable_type, value)) {
|
||||
incompatibleTypesError(variable_type, value, tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens(), `Field, variable or method call expected`));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {JavaType} value_type
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function incompatibleTypesError(variable_type, value_type, tokens, problems) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Expression of type '${value_type.fullyDottedTypeName}' cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {LambdaType} value
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkLambdaAssignable(variable_type, value, tokens, problems) {
|
||||
const res = isLambdaAssignable(variable_type, value);
|
||||
if (res === true) {
|
||||
return;
|
||||
}
|
||||
switch (res[0]) {
|
||||
case 'non-interface':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Cannot assign lambda expression to type '${variable_type.fullyDottedTypeName}'`));
|
||||
return;
|
||||
case 'no-methods':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface '${variable_type.fullyDottedTypeName}' contains no abstract methods compatible with the specified lambda expression`));
|
||||
return;
|
||||
case 'param-count':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter counts`));
|
||||
return;
|
||||
case 'bad-param':
|
||||
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter types`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {LambdaType} value
|
||||
*/
|
||||
function isLambdaAssignable(variable_type, value) {
|
||||
if (!(variable_type instanceof CEIType) || variable_type.typeKind !== 'interface') {
|
||||
return ['non-interface'];
|
||||
}
|
||||
// the functional interface must only contain one abstract method excluding public Object methods
|
||||
// and ignoring type-compatible methods from superinterfaces.
|
||||
// this is quite complicated to calculate, so for now, just check against the most common case: a simple interface type with
|
||||
// a single abstract method
|
||||
if (variable_type.supers.length > 1) {
|
||||
return true;
|
||||
}
|
||||
if (variable_type.methods.length === 0) {
|
||||
return ['no-methods']
|
||||
}
|
||||
if (variable_type.methods.length > 1) {
|
||||
return true;
|
||||
}
|
||||
const intf_method = variable_type.methods[0];
|
||||
const intf_params = intf_method.parameters;
|
||||
if (intf_params.length !== value.param_types.length) {
|
||||
return ['param-count'];
|
||||
}
|
||||
|
||||
for (let i = 0; i < intf_params.length; i++) {
|
||||
// explicit parameter types must match exactly
|
||||
if (value.param_types[i] instanceof AnyType) {
|
||||
continue;
|
||||
}
|
||||
if (intf_params[i].type instanceof AnyType) {
|
||||
continue;
|
||||
}
|
||||
if (intf_params[i].type.typeSignature !== value.param_types[i].typeSignature) {
|
||||
return ['bad-param']
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} variable_type
|
||||
* @param {ArrayValueType} value_type
|
||||
* @param {() => Token|Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkArrayLiteral(variable_type, value_type, tokens, problems) {
|
||||
if (!(variable_type instanceof ArrayType)) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Array expression cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
|
||||
return;
|
||||
}
|
||||
if (value_type.elements.length === 0) {
|
||||
// empty arrays are compatible with all array types
|
||||
return;
|
||||
}
|
||||
const element_type = variable_type.elementType;
|
||||
value_type.elements.forEach(element => {
|
||||
checkArrayElement(element_type, element.value, element.tokens);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {JavaType} element_type
|
||||
* @param {ResolvedValue} value_type
|
||||
* @param {Token[]} tokens
|
||||
*/
|
||||
function checkArrayElement(element_type, value_type, tokens) {
|
||||
if (value_type instanceof NumberLiteral) {
|
||||
if (!value_type.isCompatibleWith(element_type)) {
|
||||
incompatibleTypesError(element_type, value_type.type, () => tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value_type instanceof JavaType) {
|
||||
if (!isTypeAssignable(element_type, value_type)) {
|
||||
incompatibleTypesError(element_type, value_type, () => tokens, problems);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value_type instanceof ArrayValueType) {
|
||||
checkArrayLiteral(element_type, value_type, () => tokens, problems);
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens, `Expression expected`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {ResolvedIdent} d
|
||||
* @param {'index'|'dimension'} kind
|
||||
*/
|
||||
function checkArrayIndex(ri, d, kind) {
|
||||
const idx = d.resolveExpression(ri);
|
||||
if (idx instanceof NumberLiteral) {
|
||||
if (!idx.isCompatibleWith(PrimitiveType.map.I)) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Value '${idx.toNumber()}' is not valid as an array ${kind}`));
|
||||
}
|
||||
else if (idx.toNumber() < 0) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Negative array ${kind}: ${idx.toNumber()}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (idx instanceof PrimitiveType) {
|
||||
if (!/^[BSI]$/.test(idx.typeSignature)) {
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Expression of type '${idx.label}' is not valid as an array ${kind}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(d.tokens, `Integer value expected`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of regexes to map source primitives to their destination types.
|
||||
* eg, long (J) is type-assignable to long, float and double (and their boxed counterparts)
|
||||
* Note that void (V) is never type-assignable to anything
|
||||
*/
|
||||
const valid_primitive_types = {
|
||||
// conversions from a primitive to a value
|
||||
from: {
|
||||
B: /^[BSIJFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double);$/,
|
||||
S: /^[SIJFD]$|^Ljava\/lang\/(Short|Integer|Long|Float|Double);$/,
|
||||
I: /^[IJFD]$|^Ljava\/lang\/(Integer|Long|Float|Double);$/,
|
||||
J: /^[JFD]$|^Ljava\/lang\/(Long|Float|Double);$/,
|
||||
F: /^[FD]$|^Ljava\/lang\/(Float|Double);$/,
|
||||
D: /^D$|^Ljava\/lang\/(Double);$/,
|
||||
C: /^[CIJFD]$|^Ljava\/lang\/(Character|Integer|Long|Float|Double);$/,
|
||||
Z: /^Z$|^Ljava\/lang\/(Boolean);$/,
|
||||
V: /$^/, // V.test() always returns false
|
||||
},
|
||||
// conversions to a primitive from a value
|
||||
to: {
|
||||
B: /^[B]$|^Ljava\/lang\/(Byte);$/,
|
||||
S: /^[BS]$|^Ljava\/lang\/(Byte|Short);$/,
|
||||
I: /^[BSIC]$|^Ljava\/lang\/(Byte|Short|Integer|Character);$/,
|
||||
J: /^[BSIJC]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character);$/,
|
||||
F: /^[BSIJCF]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float);$/,
|
||||
D: /^[BSIJCFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|Double);$/,
|
||||
C: /^C$|^Ljava\/lang\/(Character);$/,
|
||||
Z: /^Z$|^Ljava\/lang\/(Boolean);$/,
|
||||
V: /$^/, // V.test() always returns false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a value of value_type is assignable to a variable of dest_type
|
||||
* @param {JavaType} dest_type
|
||||
* @param {JavaType|NumberLiteral|LambdaType|MultiValueType} value_type
|
||||
*/
|
||||
function isTypeAssignable(dest_type, value_type) {
|
||||
|
||||
if (value_type instanceof NumberLiteral) {
|
||||
return value_type.isCompatibleWith(dest_type);
|
||||
}
|
||||
|
||||
if (value_type instanceof LambdaType) {
|
||||
return isLambdaAssignable(dest_type, value_type) === true;
|
||||
}
|
||||
|
||||
if (value_type instanceof MultiValueType) {
|
||||
return value_type.types.every(t => {
|
||||
if (t instanceof JavaType || t instanceof NumberLiteral || t instanceof LambdaType || t instanceof MultiValueType)
|
||||
return isTypeAssignable(dest_type, t);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
let is_assignable = false;
|
||||
if (dest_type.typeSignature === value_type.typeSignature) {
|
||||
// exact signature match
|
||||
is_assignable = true;
|
||||
} else if (dest_type instanceof AnyType || value_type instanceof AnyType) {
|
||||
// everything is assignable to or from AnyType
|
||||
is_assignable = true;
|
||||
} else if (dest_type.rawTypeSignature === 'Ljava/lang/Object;') {
|
||||
// everything is assignable to Object
|
||||
is_assignable = true;
|
||||
} else if (value_type instanceof PrimitiveType) {
|
||||
// primitive values can only be assigned to wider primitives or their class equivilents
|
||||
is_assignable = valid_primitive_types.from[value_type.typeSignature].test(dest_type.typeSignature);
|
||||
} else if (dest_type instanceof PrimitiveType) {
|
||||
// primitive variables can only be assigned from narrower primitives or their class equivilents
|
||||
is_assignable = valid_primitive_types.to[dest_type.typeSignature].test(value_type.typeSignature);
|
||||
} else if (value_type instanceof NullType) {
|
||||
// null is assignable to any non-primitive
|
||||
is_assignable = !(dest_type instanceof PrimitiveType);
|
||||
} else if (value_type instanceof ArrayType) {
|
||||
// arrays are assignable to other arrays with the same dimensionality and type-assignable bases
|
||||
is_assignable = dest_type instanceof ArrayType
|
||||
&& dest_type.arrdims === value_type.arrdims
|
||||
&& isTypeAssignable(dest_type.base, value_type.base);
|
||||
} else if (value_type instanceof CEIType && dest_type instanceof CEIType) {
|
||||
// class/interfaces types are assignable to any class/interface types in their inheritence tree
|
||||
const valid_types = getTypeInheritanceList(value_type);
|
||||
is_assignable = valid_types.includes(dest_type);
|
||||
if (!is_assignable) {
|
||||
// generic types are also assignable to their raw counterparts
|
||||
const valid_raw_types = valid_types.map(t => t.getRawType());
|
||||
is_assignable = valid_raw_types.includes(dest_type);
|
||||
if (!is_assignable) {
|
||||
// generic types are also assignable to compatible wildcard type bounds
|
||||
const raw_type = valid_raw_types.find(rt => rt.rawTypeSignature === dest_type.rawTypeSignature);
|
||||
if (raw_type instanceof CEIType && raw_type.typeVariables.length === value_type.typeVariables.length) {
|
||||
is_assignable = dest_type.typeVariables.every((dest_tv, idx) => isTypeArgumentCompatible(dest_tv, value_type.typeVariables[idx].type));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (dest_type instanceof TypeVariableType) {
|
||||
is_assignable = !(value_type instanceof PrimitiveType || value_type instanceof NullType);
|
||||
}
|
||||
return is_assignable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TypeVariable} dest_typevar
|
||||
* @param {JavaType} value_typevar_type
|
||||
*/
|
||||
function isTypeArgumentCompatible(dest_typevar, value_typevar_type) {
|
||||
if (dest_typevar.type instanceof WildcardType) {
|
||||
if (!dest_typevar.type.bound) {
|
||||
// unbounded wildcard types are compatible with everything
|
||||
return true;
|
||||
}
|
||||
if (dest_typevar.type.bound.type === value_typevar_type) {
|
||||
return true;
|
||||
}
|
||||
switch (dest_typevar.type.bound.kind) {
|
||||
case 'extends':
|
||||
return isTypeAssignable(dest_typevar.type.bound.type, value_typevar_type);
|
||||
case 'super':;
|
||||
return isTypeAssignable(value_typevar_type, dest_typevar.type.bound.type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (value_typevar_type instanceof TypeVariableType) {
|
||||
// inferred type arguments of the form `x = List<>` are compatible with every destination type variable
|
||||
return value_typevar_type.typeVariable instanceof InferredTypeArgument;
|
||||
}
|
||||
return isTypeAssignable(dest_typevar.type, value_typevar_type);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ResolvedValue} value
|
||||
* @param {() => Token[]} tokens
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkBooleanBranchCondition(value, tokens, problems) {
|
||||
if (value instanceof JavaType) {
|
||||
if (!isTypeAssignable(PrimitiveType.map.Z, value)) {
|
||||
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected, but type '${value.fullyDottedTypeName}' found.`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected.`));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {CEIType} type
|
||||
*/
|
||||
function getTypeInheritanceList(type) {
|
||||
const types = {
|
||||
/** @type {JavaType[]} */
|
||||
list: [type],
|
||||
/** @type {Set<JavaType>} */
|
||||
done: new Set(),
|
||||
};
|
||||
let object = null;
|
||||
for (let type; type = types.list.shift(); ) {
|
||||
// always add Object last
|
||||
if (type.rawTypeSignature === 'Ljava/lang/Object;') {
|
||||
object = type;
|
||||
continue;
|
||||
}
|
||||
if (types.done.has(type)) {
|
||||
continue;
|
||||
}
|
||||
types.done.add(type);
|
||||
if (type instanceof CEIType)
|
||||
types.list.push(...type.supers);
|
||||
}
|
||||
if (object) {
|
||||
types.done.add(object);
|
||||
}
|
||||
return Array.from(types.done);
|
||||
}
|
||||
|
||||
exports.checkArrayIndex = checkArrayIndex;
|
||||
exports.checkAssignment = checkAssignment;
|
||||
exports.checkBooleanBranchCondition = checkBooleanBranchCondition;
|
||||
exports.checkTypeAssignable = checkTypeAssignable;
|
||||
exports.getTypeInheritanceList = getTypeInheritanceList;
|
||||
exports.isTypeAssignable = isTypeAssignable;
|
||||
38
langserver/java/expressiontypes/ArrayIndexExpression.js
Normal file
38
langserver/java/expressiontypes/ArrayIndexExpression.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayType } = require('java-mti');
|
||||
const { checkArrayIndex } = require('../expression-resolver');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
class ArrayIndexExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {ResolvedIdent} index
|
||||
*/
|
||||
constructor(instance, index) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.instance.tokens, ...this.index.tokens];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const instance_type = this.instance.resolveExpression(ri);
|
||||
checkArrayIndex(ri, this.index, 'index');
|
||||
if (instance_type instanceof ArrayType) {
|
||||
return instance_type.elementType;
|
||||
}
|
||||
return AnyType.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
exports.ArrayIndexExpression = ArrayIndexExpression;
|
||||
35
langserver/java/expressiontypes/ArrayValueExpression.js
Normal file
35
langserver/java/expressiontypes/ArrayValueExpression.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayValueType } = require('../anys');
|
||||
|
||||
class ArrayValueExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent[]} elements
|
||||
* @param {Token} open
|
||||
*/
|
||||
constructor(elements, open) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return new ArrayValueType(this.elements.map(e => ({
|
||||
tokens: e.tokens,
|
||||
value: e.resolveExpression(ri),
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
exports.ArrayValueExpression = ArrayValueExpression;
|
||||
200
langserver/java/expressiontypes/BinaryOpExpression.js
Normal file
200
langserver/java/expressiontypes/BinaryOpExpression.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
const { checkTypeAssignable } = require('../expression-resolver');
|
||||
|
||||
class BinaryOpExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} lhs
|
||||
* @param {Token} op
|
||||
* @param {ResolvedIdent} rhs
|
||||
*/
|
||||
constructor(lhs, op, rhs) {
|
||||
super();
|
||||
this.lhs = lhs;
|
||||
this.op = op;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const operator = this.op.value;
|
||||
const lhsvalue = this.lhs.resolveExpression(ri);
|
||||
const rhsvalue = this.rhs.resolveExpression(ri);
|
||||
|
||||
if (lhsvalue instanceof AnyType || rhsvalue instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (lhsvalue instanceof NumberLiteral || rhsvalue instanceof NumberLiteral) {
|
||||
if (lhsvalue instanceof NumberLiteral && rhsvalue instanceof NumberLiteral) {
|
||||
// if they are both literals, compute the result
|
||||
if (/^[*/%+-]$/.test(operator)) {
|
||||
return NumberLiteral[operator](lhsvalue, rhsvalue);
|
||||
}
|
||||
if (/^([&|^]|<<|>>>?)$/.test(operator) && !/[FD]/.test(`${lhsvalue.type.typeSignature}${rhsvalue.type.typeSignature}`)) {
|
||||
return NumberLiteral[operator](lhsvalue, rhsvalue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'instanceof') {
|
||||
if (!(rhsvalue instanceof TypeIdentType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Type expected`));
|
||||
}
|
||||
if (!(lhsvalue instanceof JavaType || lhsvalue instanceof NumberLiteral)) {
|
||||
ri.problems.push(ParseProblem.Error(this.lhs.tokens, `Expression expected`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (/^([*/%&|^+-]?=|<<=|>>>?=)$/.test(operator)) {
|
||||
let src_type = rhsvalue;
|
||||
if (operator.length > 1) {
|
||||
const result_types = checkOperator(operator.slice(0,-1), ri, this.op, lhsvalue, rhsvalue);
|
||||
src_type = Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
|
||||
}
|
||||
if (lhsvalue instanceof JavaType) {
|
||||
checkTypeAssignable(lhsvalue, src_type, () => this.rhs.tokens, ri.problems);
|
||||
// result of assignments are lhs type
|
||||
return lhsvalue;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.op, `Invalid assignment`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
const result_types = checkOperator(operator, ri, this.op, lhsvalue, rhsvalue);
|
||||
return Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.lhs.tokens, this.op, ...this.rhs.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operator
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Token} operator_token
|
||||
* @param {ResolvedValue} lhstype
|
||||
* @param {ResolvedValue} rhstype
|
||||
* @returns {JavaType|JavaType[]}
|
||||
*/
|
||||
function checkOperator(operator, ri, operator_token, lhstype, rhstype) {
|
||||
|
||||
if (lhstype instanceof MultiValueType) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [];
|
||||
lhstype.types.reduce((arr, type) => {
|
||||
const types = checkOperator(operator, ri, operator_token, type, rhstype);
|
||||
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
|
||||
return arr;
|
||||
}, types);
|
||||
types = [...new Set(types)];
|
||||
return types.length === 1 ? types[0] : types;
|
||||
}
|
||||
|
||||
if (rhstype instanceof MultiValueType) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [];
|
||||
rhstype.types.reduce((arr, type) => {
|
||||
const types = checkOperator(operator, ri, operator_token, lhstype, type);
|
||||
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
|
||||
return arr;
|
||||
}, types);
|
||||
types = [...new Set(types)];
|
||||
return types.length === 1 ? types[0] : types;
|
||||
}
|
||||
|
||||
if (lhstype instanceof NumberLiteral) {
|
||||
lhstype = lhstype.type;
|
||||
}
|
||||
if (rhstype instanceof NumberLiteral) {
|
||||
rhstype = rhstype.type;
|
||||
}
|
||||
|
||||
if (!(lhstype instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
if (!(rhstype instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
const typekey = `${lhstype.typeSignature}#${rhstype.typeSignature}`;
|
||||
|
||||
if (operator === '+' && /(^|#)Ljava\/lang\/String;/.test(typekey)) {
|
||||
// string appending is compatible with all types
|
||||
return ri.typemap.get('java/lang/String');
|
||||
}
|
||||
|
||||
if (/^[*/%+-]$/.test(operator)) {
|
||||
// math operators - must be numeric
|
||||
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^(D|F#[^D]|J#[^FD]|I#[^JFD])/.test(typekey)) {
|
||||
return lhstype;
|
||||
}
|
||||
if (/^(.#D|.#F|.#J|.#I)/.test(typekey)) {
|
||||
return rhstype;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^(<<|>>>?)$/.test(operator)) {
|
||||
// shift operators - must be integral
|
||||
if (!/^[BSIJC]#[BSIJC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^J/.test(typekey)) {
|
||||
return PrimitiveType.map.J;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^[&|^]$/.test(operator)) {
|
||||
// bitwise or logical operators
|
||||
if (!/^[BSIJC]#[BSIJC]$|^Z#Z$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
if (/^[JZ]/.test(typekey)) {
|
||||
return lhstype;
|
||||
}
|
||||
return PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^(&&|\|\|)$/.test(operator)) {
|
||||
// logical operators
|
||||
if (!/^Z#Z$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (/^(>=?|<=?)$/.test(operator)) {
|
||||
// numeric comparison operators
|
||||
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
// comparison operators
|
||||
if (typekey === 'Ljava/lang/String;#Ljava/lang/String;') {
|
||||
ri.problems.push(ParseProblem.Warning(operator_token, `Using equality operators '=='/'!=' to compare strings has unpredictable behaviour. Consider using String.equals(...) instead.`));
|
||||
}
|
||||
return PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
exports.BinaryOpExpression = BinaryOpExpression;
|
||||
28
langserver/java/expressiontypes/BracketedExpression.js
Normal file
28
langserver/java/expressiontypes/BracketedExpression.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
|
||||
class BracketedExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(expression) {
|
||||
super();
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.expression.resolveExpression(ri);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.expression.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
exports.BracketedExpression = BracketedExpression;
|
||||
137
langserver/java/expressiontypes/CastExpression.js
Normal file
137
langserver/java/expressiontypes/CastExpression.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { JavaType, PrimitiveType, NullType, CEIType, ArrayType } = require('java-mti');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
|
||||
class CastExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} castType
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(castType, expression) {
|
||||
super();
|
||||
this.castType = castType;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const cast_type = this.castType.resolveExpression(ri);
|
||||
if (cast_type instanceof TypeIdentType) {
|
||||
const expr_type = this.expression.resolveExpression(ri);
|
||||
checkCastable(this, cast_type.type, expr_type, ri.problems);
|
||||
return cast_type.type;
|
||||
}
|
||||
if (cast_type instanceof AnyType) {
|
||||
return cast_type;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.castType.tokens, 'Type expected'))
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.castType.tokens, ...this.expression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CastExpression} cast
|
||||
* @param {JavaType} cast_type
|
||||
* @param {ResolvedValue} expr_type
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkCastable(cast, cast_type, expr_type, problems) {
|
||||
if (expr_type instanceof JavaType) {
|
||||
if (!isTypeCastable(expr_type, cast_type)) {
|
||||
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: An expression of type '${expr_type.fullyDottedTypeName}' cannot be cast to type '${cast_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (expr_type instanceof NumberLiteral) {
|
||||
checkCastable(cast, cast_type, expr_type.type, problems);
|
||||
return;
|
||||
}
|
||||
if (expr_type instanceof MultiValueType) {
|
||||
expr_type.types.forEach(type => checkCastable(cast, cast_type, type, problems));
|
||||
return;
|
||||
}
|
||||
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: expression is not a value or variable`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType} source_type
|
||||
* @param {JavaType} cast_type
|
||||
*/
|
||||
function isTypeCastable(source_type, cast_type) {
|
||||
if (source_type.typeSignature === 'Ljava/lang/Object;') {
|
||||
// everything is castable from Object
|
||||
return true;
|
||||
}
|
||||
if (cast_type.typeSignature === 'Ljava/lang/Object;') {
|
||||
// everything is castable to Object
|
||||
return true;
|
||||
}
|
||||
if (source_type instanceof NullType) {
|
||||
// null is castable to any non-primitive
|
||||
return !(cast_type instanceof PrimitiveType);
|
||||
}
|
||||
if (source_type instanceof CEIType && cast_type instanceof CEIType) {
|
||||
if (source_type.typeKind === 'interface') {
|
||||
// interfaces are castable to any non-final class type (derived types might implement the interface)
|
||||
if (cast_type.typeKind === 'class' && !cast_type.modifiers.includes('final')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// for other class casts, one type must be assignable to the other
|
||||
if (isTypeAssignable(source_type, cast_type)) {
|
||||
return true;
|
||||
}
|
||||
if (isTypeAssignable(cast_type, source_type)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (cast_type instanceof PrimitiveType) {
|
||||
// source type must be a compatible primitive or class
|
||||
switch (cast_type.typeSignature) {
|
||||
case 'B':
|
||||
case 'S':
|
||||
case 'I':
|
||||
case 'J':
|
||||
case 'C':
|
||||
case 'F':
|
||||
case 'D':
|
||||
return /^([BSIJCFD]|Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|Double);)$/.test(source_type.typeSignature);
|
||||
case 'Z':
|
||||
return /^([Z]|Ljava\/lang\/(Boolean);)$/.test(source_type.typeSignature);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (cast_type instanceof ArrayType) {
|
||||
// the source type must have the same array dimensionality and have a castable base type
|
||||
if (source_type instanceof ArrayType) {
|
||||
if (source_type.arrdims === cast_type.arrdims) {
|
||||
if (isTypeCastable(source_type.base, cast_type.base)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source_type instanceof AnyType || cast_type instanceof AnyType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
exports.CastExpression = CastExpression;
|
||||
25
langserver/java/expressiontypes/Expression.js
Normal file
25
langserver/java/expressiontypes/Expression.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
|
||||
class Expression {
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
throw new Error('Expression.resolveExpression');
|
||||
}
|
||||
|
||||
/** @returns {Token|Token[]} */
|
||||
tokens() {
|
||||
throw new Error('Expression.tokens');
|
||||
}
|
||||
}
|
||||
|
||||
exports.Expression = Expression;
|
||||
41
langserver/java/expressiontypes/IncDecExpression.js
Normal file
41
langserver/java/expressiontypes/IncDecExpression.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
class IncDecExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} expr
|
||||
* @param {Token} operator
|
||||
* @param {'prefix'|'postfix'} which
|
||||
*/
|
||||
constructor(expr, operator, which) {
|
||||
super();
|
||||
this.expr = expr;
|
||||
this.operator = operator;
|
||||
this.which = which;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const type = this.expr.resolveExpression(ri);
|
||||
if (type instanceof PrimitiveType) {
|
||||
if (/^[BSIJFD]$/.test(type.typeSignature)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.operator;
|
||||
}
|
||||
}
|
||||
|
||||
exports.IncDecExpression = IncDecExpression;
|
||||
50
langserver/java/expressiontypes/LambdaExpression.js
Normal file
50
langserver/java/expressiontypes/LambdaExpression.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { Block } = require('../statementtypes/Block');
|
||||
const { AnyType, LambdaType } = require('../anys');
|
||||
const { Local } = require('../body-types');
|
||||
|
||||
class LambdaExpression extends Expression {
|
||||
/**
|
||||
*
|
||||
* @param {(Local|ResolvedIdent)[]} params
|
||||
* @param {ResolvedIdent|Block} body
|
||||
*/
|
||||
constructor(params, body) {
|
||||
super();
|
||||
this.params = params;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
let return_type;
|
||||
if (this.body instanceof Block) {
|
||||
// todo - search for return statements to work out what return value the lambda has
|
||||
return_type = AnyType.Instance;
|
||||
} else {
|
||||
return_type = this.body.resolveExpression(ri);
|
||||
}
|
||||
const param_types = this.params.map(p => {
|
||||
if (p instanceof Local) {
|
||||
return p.type;
|
||||
}
|
||||
return AnyType.Instance;
|
||||
})
|
||||
return new LambdaType(param_types, return_type);
|
||||
|
||||
}
|
||||
|
||||
tokens() {
|
||||
if (this.body instanceof Block) {
|
||||
return this.body.open;
|
||||
}
|
||||
return this.body.tokens;
|
||||
}
|
||||
}
|
||||
exports.LambdaExpression = LambdaExpression;
|
||||
128
langserver/java/expressiontypes/MemberExpression.js
Normal file
128
langserver/java/expressiontypes/MemberExpression.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, CEIType, PrimitiveType } = require('java-mti');
|
||||
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('../anys');
|
||||
const { getTypeInheritanceList } = require('../expression-resolver');
|
||||
const { resolveNextPackage } = require('../type-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class MemberExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {Token} dot
|
||||
* @param {Token|null} member
|
||||
*/
|
||||
constructor(instance, dot, member) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.dot = dot;
|
||||
// member will be null for incomplete expressions
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
let instance = this.instance.resolveExpression(ri);
|
||||
|
||||
if (instance instanceof AnyType) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (instance instanceof PackageNameType) {
|
||||
this.dot.loc = { key: `fqdi:${instance.package_name}` };
|
||||
if (!this.member) {
|
||||
return instance;
|
||||
}
|
||||
this.member.loc = this.dot.loc;
|
||||
const ident = this.member.value;
|
||||
const { sub_package_name, type } = resolveNextPackage(instance.package_name, ident, ri.typemap);
|
||||
if (!type && !sub_package_name) {
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved identifier: '${ident}'`));
|
||||
}
|
||||
return type ? new TypeIdentType(type)
|
||||
: sub_package_name ? new PackageNameType(sub_package_name)
|
||||
: AnyType.Instance;
|
||||
}
|
||||
|
||||
let loc_key = `fqi`;
|
||||
if (instance instanceof TypeIdentType) {
|
||||
loc_key = 'fqs';
|
||||
instance = instance.type;
|
||||
}
|
||||
|
||||
if (!(instance instanceof JavaType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
this.dot.loc = { key: `${loc_key}:${instance.typeSignature}` };
|
||||
if (!this.member) {
|
||||
ri.problems.push(ParseProblem.Error(this.dot, `Identifier expected`));
|
||||
return instance;
|
||||
}
|
||||
|
||||
this.member.loc = this.dot.loc;
|
||||
const ident = this.member.value;
|
||||
|
||||
if (ident === 'this') {
|
||||
// if this has a type qualifier (Type.this), return the type, otherwise it's
|
||||
// and error and return AnyType
|
||||
return ((loc_key === 'fqs') && (instance instanceof CEIType)) ? instance : AnyType.Instance;
|
||||
}
|
||||
|
||||
if (ident === 'class') {
|
||||
// if this has a type qualifier (Type.class), return the Class instance, otherwise it's
|
||||
// and error and return AnyType
|
||||
if (loc_key !== 'fqs') {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
let class_type = instance;
|
||||
if (instance instanceof PrimitiveType) {
|
||||
class_type = ri.typemap.get(`java/lang/${{
|
||||
B:'Byte',S:'Short',I:'Integer',J:'Long',F:'Float',D:'Double',C:'Character',Z:'Boolean',V:'Void'
|
||||
}[instance.typeSignature]}`)
|
||||
}
|
||||
const clz = ri.typemap.get('java/lang/Class').specialise([class_type]);
|
||||
if (!ri.typemap.has(clz.shortSignature)) {
|
||||
ri.typemap.set(clz.shortSignature, clz);
|
||||
}
|
||||
return clz;
|
||||
}
|
||||
|
||||
const field = instance.fields.find(f => f.name === ident);
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
|
||||
if (!(instance instanceof CEIType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
let methods = new Map();
|
||||
getTypeInheritanceList(instance).forEach(type => {
|
||||
type.methods.forEach(m => {
|
||||
let msig;
|
||||
if (m.name === ident && !methods.has(msig = m.methodSignature)) {
|
||||
methods.set(msig, m);
|
||||
}
|
||||
})
|
||||
});
|
||||
if (methods.size > 0) {
|
||||
return new MethodType([...methods.values()]);
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}' in type '${instance.fullyDottedRawName}'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.member;
|
||||
}
|
||||
}
|
||||
|
||||
exports.MemberExpression = MemberExpression;
|
||||
320
langserver/java/expressiontypes/MethodCallExpression.js
Normal file
320
langserver/java/expressiontypes/MethodCallExpression.js
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { AnyType, AnyMethod, LambdaType, MethodType, MultiValueType } = require('../anys');
|
||||
const { ArrayType, JavaType, Method,PrimitiveType, ReifiedConstructor, ReifiedMethod, Constructor } = require('java-mti');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
const { InstanceLiteral } = require('./literals/Instance')
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { ValidateInfo } = require('../body-types');
|
||||
const { SourceConstructor } = require('../source-types');
|
||||
|
||||
class MethodCallExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} instance
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
*/
|
||||
constructor(instance, open_bracket, args, commas) {
|
||||
super();
|
||||
this.instance = instance;
|
||||
this.open_bracket = open_bracket;
|
||||
this.args = args;
|
||||
this.commas = commas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const type = this.instance.resolveExpression(ri);
|
||||
if (type instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
if (!(type instanceof MethodType)) {
|
||||
// check if this is an aleternate or super constructor call: this() / super()
|
||||
const instance = this.instance.variables[0];
|
||||
if (!(instance instanceof InstanceLiteral) || !(type instanceof JavaType)) {
|
||||
ri.problems.push(ParseProblem.Error(this.instance.tokens, `Expression is not a named method'`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
let is_ctr = false;
|
||||
if (ri instanceof ValidateInfo) {
|
||||
is_ctr = ri.method instanceof SourceConstructor;
|
||||
}
|
||||
if (is_ctr) {
|
||||
resolveConstructorCall(ri, type.constructors, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
|
||||
} else {
|
||||
ri.problems.push(ParseProblem.Error(this.instance.tokens, `'this'/'super' constructor calls can only be used as the first statement of a constructor`));
|
||||
}
|
||||
return PrimitiveType.map.V;
|
||||
}
|
||||
|
||||
return resolveMethodCall(ri, type.methods, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.instance.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Method[]} methods
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function resolveMethodCall(ri, methods, open_bracket, args, commas, tokens) {
|
||||
const resolved_args = args.map((arg,idx) => arg.resolveExpression(ri));
|
||||
|
||||
// all the arguments must be typed expressions, number literals or lambdas
|
||||
/** @type {(JavaType|NumberLiteral|LambdaType|MultiValueType)[]} */
|
||||
const arg_types = [];
|
||||
resolved_args.forEach((a, idx) => {
|
||||
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType || a instanceof MultiValueType) {
|
||||
arg_types.push(a);
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
|
||||
// use AnyType for this argument
|
||||
arg_types.push(AnyType.Instance);
|
||||
});
|
||||
|
||||
// reify any methods with type-variables
|
||||
// - lambda expressions can't be used as type arguments so just pass them as void
|
||||
// - multi-value types will dynamically chhose the type, but it's always a reference type (so assignable to Object)
|
||||
const arg_java_types = arg_types.map(a =>
|
||||
a instanceof NumberLiteral ? a.type
|
||||
: a instanceof LambdaType ? PrimitiveType.map.V
|
||||
: a instanceof MultiValueType ? ri.typemap.get('java/lang/Object')
|
||||
: a);
|
||||
const reified_methods = methods.map(m => {
|
||||
if (m.typeVariables.length) {
|
||||
m = ReifiedMethod.build(m, arg_java_types);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
// work out which methods are compatible with the call arguments
|
||||
const compatible_methods = reified_methods.filter(m => isCallCompatible(m, arg_types));
|
||||
const return_types = new Set(compatible_methods.map(m => m.returnType));
|
||||
|
||||
// store the methods and argument position for signature help
|
||||
const methodIdx = Math.max(reified_methods.indexOf(compatible_methods[0]), 0);
|
||||
open_bracket.methodCallInfo = {
|
||||
methods: reified_methods,
|
||||
methodIdx,
|
||||
argIdx: 0,
|
||||
}
|
||||
args.forEach((arg, idx) => {
|
||||
const methodCallInfo = {
|
||||
methods: reified_methods,
|
||||
methodIdx,
|
||||
argIdx: idx,
|
||||
}
|
||||
// add the info to the previous comma
|
||||
const c = commas[idx-1];
|
||||
if (c) {
|
||||
c.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
// set the info on all the tokens used in the argument
|
||||
arg.tokens.forEach(tok => {
|
||||
if (tok.methodCallInfo === null) {
|
||||
tok.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!compatible_methods[0]) {
|
||||
// if any of the arguments is AnyType, just return AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
const methodlist = reified_methods.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`No compatible method found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
|
||||
));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (compatible_methods.length > 1) {
|
||||
// if any of the arguments is AnyType, return the known return-type or AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
|
||||
}
|
||||
// see if we have an exact match
|
||||
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
|
||||
const exact_match = compatible_methods.find(m => m.methodSignature.startsWith(callsig));
|
||||
if (exact_match) {
|
||||
compatible_methods.splice(0, compatible_methods.length, exact_match);
|
||||
}
|
||||
}
|
||||
|
||||
if (compatible_methods.length > 1) {
|
||||
const methodlist = compatible_methods.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`Ambiguous method call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
|
||||
));
|
||||
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
|
||||
}
|
||||
|
||||
return compatible_methods[0].returnType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Constructor[]} constructors
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} args
|
||||
* @param {Token[]} commas
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function resolveConstructorCall(ri, constructors, open_bracket, args, commas, tokens) {
|
||||
const resolved_args = args.map(arg => arg.resolveExpression(ri));
|
||||
|
||||
// all the arguments must be typed expressions, number literals or lambdas
|
||||
/** @type {(JavaType|NumberLiteral|LambdaType)[]} */
|
||||
const arg_types = [];
|
||||
resolved_args.forEach((a, idx) => {
|
||||
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType) {
|
||||
arg_types.push(a);
|
||||
return;
|
||||
}
|
||||
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
|
||||
// use AnyType for this argument
|
||||
arg_types.push(AnyType.Instance);
|
||||
});
|
||||
|
||||
// reify any methods with type-variables
|
||||
// - lambda expressions can't be used as type arguments so just pass them as void
|
||||
const arg_java_types = arg_types.map(a =>
|
||||
a instanceof NumberLiteral ? a.type
|
||||
: a instanceof LambdaType ? PrimitiveType.map.V
|
||||
: a);
|
||||
const reifed_ctrs = constructors.map(c => {
|
||||
if (c.typeVariables.length) {
|
||||
c = ReifiedConstructor.build(c, arg_java_types);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
// work out which methods are compatible with the call arguments
|
||||
const compatible_ctrs = reifed_ctrs.filter(m => isCallCompatible(m, arg_types));
|
||||
|
||||
// store the methods and argument position for signature help
|
||||
const methodIdx = reifed_ctrs.indexOf(compatible_ctrs[0]);
|
||||
open_bracket.methodCallInfo = {
|
||||
methods: reifed_ctrs,
|
||||
methodIdx,
|
||||
argIdx: 0,
|
||||
}
|
||||
args.forEach((arg, idx) => {
|
||||
const methodCallInfo = {
|
||||
methods: reifed_ctrs,
|
||||
methodIdx,
|
||||
argIdx: idx,
|
||||
}
|
||||
// add the info to the previous comma
|
||||
const c = commas[idx-1];
|
||||
if (c) {
|
||||
c.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
// set the info on all the tokens used in the argument
|
||||
arg.tokens.forEach(tok => {
|
||||
if (tok.methodCallInfo === null) {
|
||||
tok.methodCallInfo = methodCallInfo;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!compatible_ctrs[0]) {
|
||||
// if any of the arguments is AnyType, just ignore the call
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return;
|
||||
}
|
||||
const ctrlist = reifed_ctrs.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`No compatible constructor found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (compatible_ctrs.length > 1) {
|
||||
// if any of the arguments is AnyType, return the known return-type or AnyType
|
||||
if (arg_java_types.find(t => t instanceof AnyType)) {
|
||||
return;
|
||||
}
|
||||
// see if we have an exact match
|
||||
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
|
||||
const exact_match = compatible_ctrs.find(m => m.methodSignature.startsWith(callsig));
|
||||
if (exact_match) {
|
||||
compatible_ctrs.splice(0, compatible_ctrs.length, exact_match);
|
||||
}
|
||||
}
|
||||
|
||||
if (compatible_ctrs.length > 1) {
|
||||
const ctrlist = compatible_ctrs.map(m => m.label).join('\n- ');
|
||||
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
|
||||
ri.problems.push(ParseProblem.Error(tokens(),
|
||||
`Ambiguous constructor call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Method|Constructor} m
|
||||
* @param {(JavaType | NumberLiteral | LambdaType | MultiValueType)[]} arg_types
|
||||
*/
|
||||
function isCallCompatible(m, arg_types) {
|
||||
if (m instanceof AnyMethod) {
|
||||
return true;
|
||||
}
|
||||
const param_count = m.parameterCount;
|
||||
if (param_count !== arg_types.length) {
|
||||
// for variable arity methods, we must have at least n-1 formal parameters
|
||||
if (!m.isVariableArity || arg_types.length < param_count - 1) {
|
||||
// wrong parameter count
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const formal_params = m.parameters.slice();
|
||||
const last_param = formal_params.pop();
|
||||
for (let i = 0; i < arg_types.length; i++) {
|
||||
const param = formal_params[i] || last_param;
|
||||
let param_type = param.type;
|
||||
if (param.varargs && param_type instanceof ArrayType) {
|
||||
// last varargs parameter
|
||||
// - if the argument count matches the parameter count, the final argument can match the array or non-array version
|
||||
// e.g void v(int... x) will match with v(), v(1) and v(new int[3]);
|
||||
if (arg_types.length === param_count) {
|
||||
if (isTypeAssignable(param_type, arg_types[i])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
param_type = param_type.elementType;
|
||||
}
|
||||
// is the argument assignable to the parameter
|
||||
if (isTypeAssignable(param_type, arg_types[i])) {
|
||||
continue;
|
||||
}
|
||||
// mismatch parameter type
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.MethodCallExpression = MethodCallExpression;
|
||||
exports.resolveConstructorCall = resolveConstructorCall;
|
||||
94
langserver/java/expressiontypes/NewExpression.js
Normal file
94
langserver/java/expressiontypes/NewExpression.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../source-types').AnonymousSourceType} AnonymousSourceType
|
||||
* @typedef {import('../source-types').SourceTypeIdent} SourceTypeIdent
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { ArrayType } = require('java-mti');
|
||||
const { FixedLengthArrayType, SourceArrayType } = require('../source-types');
|
||||
const { checkArrayIndex } = require('../expression-resolver');
|
||||
const { resolveConstructorCall } = require('./MethodCallExpression');
|
||||
|
||||
class NewArray extends Expression {
|
||||
/**
|
||||
* @param {Token} new_token
|
||||
* @param {SourceTypeIdent} element_type
|
||||
* @param {ResolvedIdent} dimensions
|
||||
*/
|
||||
constructor(new_token, element_type, dimensions) {
|
||||
super();
|
||||
this.new_token = new_token;
|
||||
this.element_type = element_type;
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
/** @type {ResolvedIdent[]} */
|
||||
const fixed_dimensions = [];
|
||||
const type = this.dimensions.types[0];
|
||||
for (let x = type; ;) {
|
||||
if (x instanceof FixedLengthArrayType) {
|
||||
fixed_dimensions.unshift(x.length);
|
||||
x = x.parent_type;
|
||||
continue;
|
||||
}
|
||||
if (x instanceof SourceArrayType) {
|
||||
x = x.parent_type;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const arrdims = type instanceof ArrayType ? type.arrdims : 1;
|
||||
const array_type = new ArrayType(this.element_type.resolved, arrdims);
|
||||
|
||||
fixed_dimensions.forEach(d => {
|
||||
checkArrayIndex(ri, d, 'dimension');
|
||||
})
|
||||
return array_type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.new_token, ...this.dimensions.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
class NewObject extends Expression {
|
||||
/**
|
||||
* @param {Token} new_token
|
||||
* @param {SourceTypeIdent} object_type
|
||||
* @param {Token} open_bracket
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {Token[]} commas
|
||||
* @param {AnonymousSourceType} type_body
|
||||
*/
|
||||
constructor(new_token, object_type, open_bracket, ctr_args, commas, type_body) {
|
||||
super();
|
||||
this.new_token = new_token;
|
||||
this.object_type = object_type;
|
||||
this.open_bracket = open_bracket;
|
||||
this.ctr_args = ctr_args;
|
||||
this.commas = commas;
|
||||
this.type_body = type_body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
resolveConstructorCall(ri, this.object_type.resolved.constructors, this.open_bracket, this.ctr_args, this.commas, () => this.tokens());
|
||||
return this.object_type.resolved;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.new_token, ...this.object_type.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
exports.NewArray = NewArray;
|
||||
exports.NewObject = NewObject;
|
||||
35
langserver/java/expressiontypes/TernaryOpExpression.js
Normal file
35
langserver/java/expressiontypes/TernaryOpExpression.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { MultiValueType } = require('../anys');
|
||||
|
||||
class TernaryOpExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} test
|
||||
* @param {ResolvedIdent} truthExpression
|
||||
* @param {ResolvedIdent} falseExpression
|
||||
*/
|
||||
constructor(test, truthExpression, falseExpression) {
|
||||
super();
|
||||
this.test = test;
|
||||
this.truthExpression = truthExpression;
|
||||
this.falseExpression = falseExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const ttype = this.truthExpression.resolveExpression(ri);
|
||||
const ftype = this.falseExpression.resolveExpression(ri);
|
||||
return new MultiValueType(ttype, ftype);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [...this.test.tokens, ...this.truthExpression.tokens, ...this.falseExpression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
exports.TernaryOpExpression = TernaryOpExpression;
|
||||
96
langserver/java/expressiontypes/UnaryOpExpression.js
Normal file
96
langserver/java/expressiontypes/UnaryOpExpression.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType } = require('../anys');
|
||||
const { NumberLiteral } = require('./literals/Number');
|
||||
|
||||
class UnaryOpExpression extends Expression {
|
||||
/**
|
||||
* @param {ResolvedIdent} expression
|
||||
* @param {Token} op
|
||||
*/
|
||||
constructor(expression, op) {
|
||||
super();
|
||||
this.expression = expression;
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
const operator = this.op.value;
|
||||
const value = this.expression.resolveExpression(ri);
|
||||
|
||||
if (value instanceof AnyType) {
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
if (value instanceof NumberLiteral) {
|
||||
if (/^[+-]$/.test(operator)) {
|
||||
return NumberLiteral[operator](value);
|
||||
}
|
||||
if (/^[!~]$/.test(operator) && value.type.typeSignature === 'I') {
|
||||
return NumberLiteral[operator](value);
|
||||
}
|
||||
}
|
||||
|
||||
const type = value instanceof JavaType ? value : value instanceof NumberLiteral ? value.type : null;
|
||||
|
||||
if (!type) {
|
||||
ri.problems.push(ParseProblem.Error(this.expression.tokens, `Expression expected`));
|
||||
return AnyType.Instance;
|
||||
}
|
||||
|
||||
return checkOperator(operator, ri, this.op, type);
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return [this.op, ...this.expression.tokens];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operator
|
||||
* @param {ResolveInfo} ri
|
||||
* @param {Token} operator_token
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
function checkOperator(operator, ri, operator_token, type) {
|
||||
|
||||
let is_valid = false;
|
||||
/** @type {JavaType} */
|
||||
let return_type = AnyType.Instance;
|
||||
|
||||
if (/^[+-]$/.test(operator)) {
|
||||
// math operators - must be numeric
|
||||
is_valid = /^[BSIJFDC]$/.test(type.typeSignature);
|
||||
return_type = type;
|
||||
}
|
||||
|
||||
if (/^~$/.test(operator)) {
|
||||
// bitwise invert operator - must be integral
|
||||
is_valid = /^[BSIJC]$/.test(type.typeSignature);
|
||||
return_type = PrimitiveType.map.I;
|
||||
}
|
||||
|
||||
if (/^!$/.test(operator)) {
|
||||
// logical not operator - must be boolean
|
||||
is_valid = /^Z$/.test(type.typeSignature);
|
||||
return_type = PrimitiveType.map.Z;
|
||||
}
|
||||
|
||||
if (!is_valid) {
|
||||
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for type '${type.fullyDottedTypeName}'`));
|
||||
}
|
||||
|
||||
return return_type;
|
||||
}
|
||||
|
||||
exports.UnaryOpExpression = UnaryOpExpression;
|
||||
36
langserver/java/expressiontypes/Variable.js
Normal file
36
langserver/java/expressiontypes/Variable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').Field} Field
|
||||
* @typedef {import('java-mti').Parameter} Parameter
|
||||
* @typedef {import('../source-types').SourceEnumValue} SourceEnumValue
|
||||
*/
|
||||
const { Expression } = require("./Expression");
|
||||
|
||||
class Variable extends Expression {
|
||||
/**
|
||||
* @param {Token} name_token
|
||||
* @param {Local|Parameter|Field|SourceEnumValue} variable
|
||||
*/
|
||||
constructor(name_token, variable) {
|
||||
super();
|
||||
this.name_token = name_token;
|
||||
this.variable = variable;
|
||||
this.type = this.variable.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this.name_token;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Variable = Variable;
|
||||
17
langserver/java/expressiontypes/literals/Boolean.js
Normal file
17
langserver/java/expressiontypes/literals/Boolean.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
class BooleanLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, PrimitiveType.map.Z);
|
||||
}
|
||||
}
|
||||
|
||||
exports.BooleanLiteral = BooleanLiteral;
|
||||
17
langserver/java/expressiontypes/literals/Character.js
Normal file
17
langserver/java/expressiontypes/literals/Character.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
class CharacterLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, PrimitiveType.map.C);
|
||||
}
|
||||
}
|
||||
|
||||
exports.CharacterLiteral = CharacterLiteral;
|
||||
31
langserver/java/expressiontypes/literals/Instance.js
Normal file
31
langserver/java/expressiontypes/literals/Instance.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
|
||||
class InstanceLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token 'this' or 'super' token
|
||||
* @param {CEIType} scoped_type
|
||||
*/
|
||||
constructor(token, scoped_type) {
|
||||
super(token, null);
|
||||
this.token = token;
|
||||
this.scoped_type = scoped_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
if (this.token.value === 'this') {
|
||||
return this.scoped_type;
|
||||
}
|
||||
return this.scoped_type.supers.find(t => t.typeKind === 'class') || ri.typemap.get('java/lang/Object');
|
||||
}
|
||||
}
|
||||
|
||||
exports.InstanceLiteral = InstanceLiteral;
|
||||
33
langserver/java/expressiontypes/literals/LiteralValue.js
Normal file
33
langserver/java/expressiontypes/literals/LiteralValue.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('../../anys').ResolvedValue} ResolvedValue
|
||||
*/
|
||||
const { Expression } = require('../Expression');
|
||||
|
||||
class LiteralValue extends Expression {
|
||||
/**
|
||||
* @param {Token|Token[]} tokens
|
||||
* @param {JavaType} known_type
|
||||
*/
|
||||
constructor(tokens, known_type) {
|
||||
super();
|
||||
this._tokens = tokens;
|
||||
this.type = known_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
* @returns {ResolvedValue}
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
tokens() {
|
||||
return this._tokens;
|
||||
}
|
||||
}
|
||||
|
||||
exports.LiteralValue = LiteralValue;
|
||||
17
langserver/java/expressiontypes/literals/Null.js
Normal file
17
langserver/java/expressiontypes/literals/Null.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { NullType } = require('java-mti');
|
||||
|
||||
class NullLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(token) {
|
||||
super(token, new NullType());
|
||||
}
|
||||
}
|
||||
|
||||
exports.NullLiteral = NullLiteral;
|
||||
236
langserver/java/expressiontypes/literals/Number.js
Normal file
236
langserver/java/expressiontypes/literals/Number.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
const { PrimitiveType } = require('java-mti');
|
||||
|
||||
/**
|
||||
* NumberLiteral is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc).
|
||||
*
|
||||
* It allows literal numbers to be type-assignable to variables with different primitive types.
|
||||
* For example, 200 is type-assignable to short, int, long, float and double, but not byte.
|
||||
*/
|
||||
class NumberLiteral extends LiteralValue {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {string} kind
|
||||
* @param {PrimitiveType} default_type
|
||||
* @param {string} [value]
|
||||
*/
|
||||
constructor(tokens, kind, default_type, value = tokens[0].value) {
|
||||
super(tokens, default_type);
|
||||
this.value = value;
|
||||
this.numberKind = kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveInfo} ri
|
||||
*/
|
||||
resolveExpression(ri) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {string} kind
|
||||
* @param {PrimitiveType} type
|
||||
* @param {number} value
|
||||
*/
|
||||
static calc(a, b, kind, type, value) {
|
||||
let atoks = a.tokens(), btoks = b.tokens();
|
||||
atoks = Array.isArray(atoks) ? atoks : [atoks];
|
||||
btoks = Array.isArray(btoks) ? btoks : [btoks];
|
||||
return new NumberLiteral([...atoks, ...btoks], kind, type, value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
static shift(a, b, op) {
|
||||
const ai = a.toInt(), bi = b.toInt();
|
||||
if (ai === null || bi === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai, bi);
|
||||
const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I;
|
||||
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
static bitwise(a, b, op) {
|
||||
const ai = a.toInt(), bi = b.toInt();
|
||||
if (ai === null || bi === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai, bi);
|
||||
const typekey = a.type.typeSignature+ b.type.typeSignature;
|
||||
const type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I;
|
||||
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {string} opvalue
|
||||
* @param {(a) => Number} op
|
||||
*/
|
||||
static unary(a, opvalue, op) {
|
||||
if (opvalue === '-') {
|
||||
const ai = a.toNumber();
|
||||
if (ai === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai);
|
||||
const type = PrimitiveType.map[a.type.typeSignature];
|
||||
const toks = a.tokens();
|
||||
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
|
||||
}
|
||||
const ai = a.toInt();
|
||||
if (ai === null) {
|
||||
return null;
|
||||
}
|
||||
const val = op(ai);
|
||||
const type = /J/.test(a.type.typeSignature) ? PrimitiveType.map.J : PrimitiveType.map.I;
|
||||
const toks = a.tokens();
|
||||
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NumberLiteral} a
|
||||
* @param {NumberLiteral} b
|
||||
* @param {(a,b) => Number} op
|
||||
*/
|
||||
static math(a, b, op) {
|
||||
const ai = a.toNumber(), bi = b.toNumber();
|
||||
let val = op(ai, bi);
|
||||
const typekey = a.type.typeSignature + b.type.typeSignature;
|
||||
if (!/[FD]/.test(typekey)) {
|
||||
val = Math.trunc(val);
|
||||
}
|
||||
const type = typekey.includes('D') ? PrimitiveType.map.D
|
||||
: typekey.includes('F') ? PrimitiveType.map.F
|
||||
: typekey.includes('J') ? PrimitiveType.map.J
|
||||
: PrimitiveType.map.I;
|
||||
// note: Java allows integer division by zero at compile-time - it will
|
||||
// always cause an ArithmeticException at runtime, so the result here (inf or nan)
|
||||
// is largely meaningless
|
||||
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
|
||||
}
|
||||
|
||||
static '~'(value) { return NumberLiteral.unary(value, '~', (a) => ~a) }
|
||||
static '+'(lhs, rhs) { return !rhs
|
||||
? lhs // unary e.g +5
|
||||
: NumberLiteral.math(lhs, rhs, (a,b) => a + b)
|
||||
}
|
||||
static '-'(lhs, rhs) { return !rhs
|
||||
? NumberLiteral.unary(lhs, '-', (a) => -a)
|
||||
: NumberLiteral.math(lhs, rhs, (a,b) => a - b)
|
||||
}
|
||||
static '*'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a * b) }
|
||||
static '/'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a / b) }
|
||||
static '%'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a % b) }
|
||||
static '&'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a & b) }
|
||||
static '|'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a | b) }
|
||||
static '^'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a ^ b) }
|
||||
static '>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a >> b) }
|
||||
static '>>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => {
|
||||
// unsigned shift (>>>) is not supported by bigints
|
||||
// @ts-ignore
|
||||
return (a >> b) & ~(-1n << (64n - b));
|
||||
}) }
|
||||
static '<<'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a << b) }
|
||||
|
||||
toInt() {
|
||||
switch (this.numberKind) {
|
||||
case 'hex-number-literal':
|
||||
case 'int-number-literal':
|
||||
// unlike parseInt, BigInt doesn't like invalid characters, so
|
||||
// ensure we strip any trailing long specifier
|
||||
return BigInt(this.value.match(/(.+?)[lL]?$/)[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
toNumber() {
|
||||
return parseFloat(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
isCompatibleWith(type) {
|
||||
if (this.type === type) {
|
||||
return true;
|
||||
}
|
||||
switch(this.type.simpleTypeName) {
|
||||
case 'double':
|
||||
return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature);
|
||||
case 'float':
|
||||
return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature);
|
||||
}
|
||||
// all integral types are all compatible with long, float and double variables
|
||||
if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) {
|
||||
return true;
|
||||
}
|
||||
// the desintation type must be a number primitive or one of the corresponding boxed classes
|
||||
if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) {
|
||||
return false;
|
||||
}
|
||||
let number = 0;
|
||||
if (this.numberKind === 'hex-number-literal') {
|
||||
if (this.value !== '0x') {
|
||||
const non_leading_zero_digits = this.value.match(/0x0*(.+)/)[1];
|
||||
number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16);
|
||||
}
|
||||
} else if (this.numberKind === 'int-number-literal') {
|
||||
const non_leading_zero_digits = this.value.match(/0*(.+)/)[1];
|
||||
number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10);
|
||||
}
|
||||
if (number >= -128 && number <= 127) {
|
||||
return true; // byte values are compatible with all other numbers
|
||||
}
|
||||
if (number >= -32768 && number <= 32767) {
|
||||
return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte
|
||||
}
|
||||
return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
static from(token) {
|
||||
function suffix(which) {
|
||||
switch(which.indexOf(token.value.slice(-1))) {
|
||||
case 0:
|
||||
case 1:
|
||||
return PrimitiveType.map.F;
|
||||
case 2:
|
||||
case 3:
|
||||
return PrimitiveType.map.D;
|
||||
case 4:
|
||||
case 5:
|
||||
return PrimitiveType.map.J;
|
||||
}
|
||||
}
|
||||
switch(token.kind) {
|
||||
case 'dec-exp-number-literal':
|
||||
case 'dec-number-literal':
|
||||
return new NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.D);
|
||||
case 'hex-number-literal':
|
||||
return new NumberLiteral([token], token.kind, suffix(' Ll') || PrimitiveType.map.I);
|
||||
case 'int-number-literal':
|
||||
default:
|
||||
return new NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.I);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.NumberLiteral = NumberLiteral;
|
||||
18
langserver/java/expressiontypes/literals/String.js
Normal file
18
langserver/java/expressiontypes/literals/String.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('../../tokenizer').Token} Token
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
const { LiteralValue } = require('./LiteralValue');
|
||||
|
||||
class StringLiteral extends LiteralValue {
|
||||
/**
|
||||
*
|
||||
* @param {Token} token
|
||||
* @param {CEIType} string_type
|
||||
*/
|
||||
constructor(token, string_type) {
|
||||
super(token, string_type);
|
||||
}
|
||||
}
|
||||
|
||||
exports.StringLiteral = StringLiteral;
|
||||
109
langserver/java/import-resolver.js
Normal file
109
langserver/java/import-resolver.js
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
const ResolvedImport = require('./parsetypes/resolved-import');
|
||||
|
||||
/**
|
||||
* Search a space-separated list of type names for values that match a dotted import.
|
||||
*
|
||||
* @param {string} typenames newline-separated list of fully qualified type names
|
||||
* @param {string} dotted_import fully-qualified import name (e.g "java.util")
|
||||
* @param {boolean} demandload true if this is a demand-load import
|
||||
*/
|
||||
function fetchImportedTypes(typenames, dotted_import, demandload) {
|
||||
const matcher = demandload
|
||||
// for demand-load, we search for any types that begin with the specified import name
|
||||
// - note that after the import text, only words and $ are allowed (because additional dots would imply a subpackage)
|
||||
? new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}[/$][\\w$]+$`, 'gm')
|
||||
// for exact-load, we search for any types that precisely matches the specified import name
|
||||
: new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}$`, 'gm');
|
||||
|
||||
// run the regex against the list of type names
|
||||
const matching_names = typenames.match(matcher);
|
||||
return matching_names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a single parsed import
|
||||
*
|
||||
* @param {Map<string, import('java-mti').CEIType>} typemap
|
||||
* @param {string} dotted_name
|
||||
* @param {boolean} is_static
|
||||
* @param {boolean} on_demand
|
||||
* @param {'owner-package'|'import'|'implicit-import'} import_kind
|
||||
*/
|
||||
function resolveSingleImport(typemap, dotted_name, is_static, on_demand, import_kind) {
|
||||
// construct the list of typenames
|
||||
const typenames = [...typemap.keys()].join('\n');
|
||||
|
||||
if (is_static) {
|
||||
if (on_demand) {
|
||||
// import all static members - the dotted name must be an exact type
|
||||
const matches = fetchImportedTypes(typenames, dotted_name, false);
|
||||
if (matches) {
|
||||
return new ResolvedImport(matches, '*', typemap, import_kind);
|
||||
}
|
||||
} else if (dotted_name.includes('.')) {
|
||||
// the final ident is the static member - the rest is the exact type
|
||||
const split_name = dotted_name.match(/(.+)\.([^.]+)$/);
|
||||
const matches = fetchImportedTypes(typenames, split_name[1], false);
|
||||
if (matches) {
|
||||
const i = new ResolvedImport(matches, split_name[2], typemap, import_kind);
|
||||
// if there's no matching member, treat it as an invalid import
|
||||
if (i.members.length > 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const matches = fetchImportedTypes(typenames, dotted_name, on_demand);
|
||||
if (matches) {
|
||||
return new ResolvedImport(matches, null, typemap, import_kind);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a set of imports for a module.
|
||||
*
|
||||
* Note that the order of the resolved imports is important for correct type resolution:
|
||||
* - same-package imports are first,
|
||||
* - followed by import declarations (in order of declaration),
|
||||
* - followed by implicit packages
|
||||
*
|
||||
* @param {Map<string, import('java-mti').CEIType>} typemap
|
||||
* @param {string} package_name package name of the module
|
||||
* @param {string[]} [implicitPackages] list of implicit demand-load packages
|
||||
*/
|
||||
function resolveImports(typemap, package_name, implicitPackages = ['java.lang']) {
|
||||
|
||||
// construct the list of typenames
|
||||
const typenames = [...typemap.keys()].join('\n');
|
||||
|
||||
/** @type {ResolvedImport[]} */
|
||||
const resolved = [];
|
||||
|
||||
// import types matching the current package
|
||||
if (package_name) {
|
||||
const matches = fetchImportedTypes(typenames, package_name, true);
|
||||
if (matches)
|
||||
resolved.push(new ResolvedImport(matches, null, typemap, 'owner-package'));
|
||||
}
|
||||
|
||||
// import types from the implicit packages
|
||||
implicitPackages.forEach(package_name => {
|
||||
const matches = fetchImportedTypes(typenames, package_name, true);
|
||||
if (matches)
|
||||
resolved.push(new ResolvedImport(matches, null, typemap, 'implicit-import'));
|
||||
})
|
||||
|
||||
/**
|
||||
* return the resolved imports.
|
||||
*/
|
||||
return resolved;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveImports,
|
||||
resolveSingleImport,
|
||||
ResolvedImport,
|
||||
}
|
||||
72
langserver/java/java-libraries.js
Normal file
72
langserver/java/java-libraries.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { CEIType, loadJavaLibraryCacheFile } = require('java-mti');
|
||||
const analytics = require('../analytics');
|
||||
const { trace, time, timeEnd } = require('../logging');
|
||||
|
||||
/**
|
||||
* @param {string} extensionPath install path of extension
|
||||
* @param {string[]} additional_libs set of androidx library names to include eg: ["androidx.activity:activity"]
|
||||
* @returns {Promise<Map<string,CEIType>>}
|
||||
*/
|
||||
async function loadAndroidSystemLibrary(extensionPath, additional_libs) {
|
||||
analytics.time('android-library-load');
|
||||
time('android-library-load');
|
||||
let library;
|
||||
try {
|
||||
if (!extensionPath) {
|
||||
throw new Error('Missing extension path')
|
||||
}
|
||||
const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
|
||||
trace(`loading android library from ${cache_folder} with androidx libs: ${JSON.stringify(additional_libs)}`)
|
||||
const typemap = await loadJavaLibraryCacheFile(path.join(cache_folder, 'android-29.zip'));
|
||||
if (Array.isArray(additional_libs) && additional_libs.length) {
|
||||
await loadJavaLibraryCacheFile(path.join(cache_folder, 'androidx-20200701.zip'), additional_libs, typemap);
|
||||
}
|
||||
trace(`loaded ${typemap.size} types into android library`);
|
||||
library = typemap;
|
||||
} finally {
|
||||
timeEnd('android-library-load');
|
||||
analytics.timeEnd('android-library-load', 'ms', { libs: additional_libs, typecount: library ? library.size : 0 });
|
||||
}
|
||||
return library;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cache_folder
|
||||
*/
|
||||
async function loadHighestAPIPlatform(cache_folder) {
|
||||
/** @type {fs.Dirent[]} */
|
||||
const files = await new Promise((res, rej) => {
|
||||
fs.readdir(cache_folder, {withFileTypes: true}, (err, files) => err ? rej(err) : res(files));
|
||||
});
|
||||
|
||||
// find the file with the highest API level
|
||||
let best_match = {
|
||||
api: 0,
|
||||
/** @type {fs.Dirent} */
|
||||
file: null,
|
||||
};
|
||||
files.forEach(file => {
|
||||
const m = file.name.match(/^android-(\d+)\.zip$/);
|
||||
if (!m) return;
|
||||
const api = parseInt(m[1], 10);
|
||||
if (api > best_match.api) {
|
||||
best_match = {
|
||||
api,
|
||||
file,
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!best_match.file) {
|
||||
throw new Error(`No valid platform cache files found in ${cache_folder}`)
|
||||
}
|
||||
console.log(`loading android platform cache: ${best_match.file.name}`);
|
||||
|
||||
const cache_file = path.join(cache_folder, best_match.file.name);
|
||||
const typemap = loadJavaLibraryCacheFile(cache_file);
|
||||
|
||||
return typemap;
|
||||
}
|
||||
|
||||
exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary;
|
||||
73
langserver/java/parsetypes/parse-problem.js
Normal file
73
langserver/java/parsetypes/parse-problem.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const ProblemSeverity = require('./problem-severity');
|
||||
const { TextBlock } = require('./textblock');
|
||||
|
||||
/**
|
||||
* @typedef {import('./problem-severity').Severity} Severity
|
||||
*/
|
||||
|
||||
|
||||
class ParseProblem {
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
* @param {Severity} severity
|
||||
*/
|
||||
constructor(token, message, severity) {
|
||||
if (!token || (Array.isArray(token) && !token[0])) {
|
||||
this.startIdx = 0;
|
||||
this.endIdx = 1;
|
||||
}
|
||||
else if (Array.isArray(token)) {
|
||||
this.startIdx = token[0].range.start;
|
||||
const lastToken = token[token.length - 1];
|
||||
this.endIdx = lastToken.range.start + lastToken.range.length;
|
||||
} else {
|
||||
this.startIdx = token.range.start;
|
||||
this.endIdx = this.startIdx + token.range.length;
|
||||
}
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Error(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Warning(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Information(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Information);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
* @param {string} message
|
||||
*/
|
||||
static Hint(token, message) {
|
||||
return new ParseProblem(token, message, ProblemSeverity.Hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextBlock|TextBlock[]} token
|
||||
*/
|
||||
static syntaxError(token) {
|
||||
if (!token) return null;
|
||||
return ParseProblem.Error(token, 'Unsupported, invalid or incomplete declaration');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ParseProblem;
|
||||
8
langserver/java/parsetypes/problem-severity.js
Normal file
8
langserver/java/parsetypes/problem-severity.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @typedef {1|2|3|4} Severity
|
||||
* @type {{ Error:1, Warning:2, Information:3, Hint:4 }}
|
||||
* these match the vscode DiagnosticSeverity values
|
||||
*/
|
||||
const ProblemSeverity = { Error:1, Warning:2, Information:3, Hint:4 };
|
||||
|
||||
module.exports = ProblemSeverity;
|
||||
49
langserver/java/parsetypes/resolved-import.js
Normal file
49
langserver/java/parsetypes/resolved-import.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a resolved import.
|
||||
*
|
||||
* Each instance holds an array of types that would be resolved by the specified import.
|
||||
* Each type is mapped to a JavaType which lists the implementation details of the type (fields, methods, etc).
|
||||
*
|
||||
*/
|
||||
class ResolvedImport {
|
||||
/**
|
||||
* @param {RegExpMatchArray} matches
|
||||
* @param {string} static_ident
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {'owner-package'|'import'|'implicit-import'} import_kind
|
||||
*/
|
||||
constructor(matches, static_ident, typemap, import_kind) {
|
||||
/**
|
||||
* Array of fully qualified type names in JRE format resolved in this import
|
||||
*/
|
||||
this.fullyQualifiedNames = Array.from(matches);
|
||||
|
||||
/**
|
||||
* THe map of fully-qualified type names to JavaTypes
|
||||
*/
|
||||
this.types = new Map(matches.map(name => [name, typemap.get(name)]));
|
||||
|
||||
this.members = [];
|
||||
if (static_ident) {
|
||||
const type = typemap.get(matches[0]);
|
||||
if (type) {
|
||||
type.fields.forEach(f => f.modifiers.includes('static') && (static_ident === '*' || static_ident === f.name) && this.members.push(f));
|
||||
type.methods.forEach(m => m.modifiers.includes('static') && (static_ident === '*' || static_ident === m.name) && this.members.push(m));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What kind of import this is:
|
||||
* - `"owner-package"`: types that are implicitly imported from the same package as the declared module
|
||||
* - `"import"`: types that are inclduded via an import declaration specified in the module
|
||||
* - `"implicit-import"`: types that are included without any explicit import (`java.lang.*` for example)
|
||||
*/
|
||||
this.import_kind = import_kind;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResolvedImport;
|
||||
141
langserver/java/parsetypes/textblock.js
Normal file
141
langserver/java/parsetypes/textblock.js
Normal file
@@ -0,0 +1,141 @@
|
||||
class BlockRange {
|
||||
|
||||
get end() { return this.start + this.length }
|
||||
get text() { return this.source.slice(this.start, this.end) }
|
||||
/**
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor(source, start, length) {
|
||||
this.source = source;
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
class TextBlock {
|
||||
/**
|
||||
* @param {BlockRange|TextBlockArray} range
|
||||
* @param {string} simplified
|
||||
*/
|
||||
constructor(range, simplified) {
|
||||
this.range = range;
|
||||
this.simplified = simplified;
|
||||
}
|
||||
|
||||
blockArray() {
|
||||
return this.range instanceof TextBlockArray ? this.range : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the original source
|
||||
* @returns {number}
|
||||
*/
|
||||
get length() {
|
||||
return this.range.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
* @param {string} [simplified]
|
||||
*/
|
||||
static from(source, start, length, simplified) {
|
||||
const range = new BlockRange(source, start, length);
|
||||
return new TextBlock(range, simplified || range.text);
|
||||
}
|
||||
|
||||
get source() { return this.toSource() }
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toSource() {
|
||||
return this.range instanceof BlockRange
|
||||
? this.range.text
|
||||
: this.range.toSource()
|
||||
}
|
||||
}
|
||||
|
||||
class TextBlockArray {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {import('../tokenizer').Token[]} [blocks]
|
||||
*/
|
||||
constructor(id, blocks = []) {
|
||||
this.id = id;
|
||||
this.blocks = blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the original source
|
||||
* @returns {number}
|
||||
*/
|
||||
get length() {
|
||||
return this.blocks.reduce(((len,b) => len + b.length), 0);
|
||||
}
|
||||
|
||||
get simplified() {
|
||||
return this.blocks.map(tb => tb.simplified).join('');
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
get start() {
|
||||
return this.blocks[0].range.start;
|
||||
}
|
||||
|
||||
sourcemap() {
|
||||
let idx = 0;
|
||||
const parts = [];
|
||||
/** @type {number[]} */
|
||||
const map = this.blocks.reduce((arr,tb,i) => {
|
||||
arr[idx] = i;
|
||||
if (!tb) {
|
||||
throw this.blocks;
|
||||
}
|
||||
parts.push(tb.simplified);
|
||||
idx += tb.simplified.length;
|
||||
return arr;
|
||||
}, []);
|
||||
map[idx] = this.blocks.length;
|
||||
return {
|
||||
simplified: parts.join(''),
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {number} start_block_idx
|
||||
* @param {number} block_count
|
||||
* @param {RegExpMatchArray} match
|
||||
* @param {string} marker
|
||||
* @param {*} [parseClass]
|
||||
* @param {boolean} [pad]
|
||||
*/
|
||||
shrink(id, start_block_idx, block_count, match, marker, parseClass, pad=true) {
|
||||
if (block_count <= 0) return;
|
||||
const collapsed = new TextBlockArray(id, this.blocks.splice(start_block_idx, block_count, null));
|
||||
const simplified = pad
|
||||
? collapsed.source.replace(/./g, ' ').replace(/^./, marker)
|
||||
: marker;
|
||||
return this.blocks[start_block_idx] = parseClass
|
||||
? new parseClass(collapsed, simplified, match)
|
||||
: new TextBlock(collapsed, simplified);
|
||||
}
|
||||
|
||||
get source() { return this.toSource() }
|
||||
|
||||
toSource() {
|
||||
return this.blocks.map(tb => tb.toSource()).join('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BlockRange,
|
||||
TextBlock,
|
||||
TextBlockArray,
|
||||
}
|
||||
756
langserver/java/source-types.js
Normal file
756
langserver/java/source-types.js
Normal file
@@ -0,0 +1,756 @@
|
||||
const { CEIType, JavaType, PrimitiveType, ArrayType, TypeVariableType, Field, Method, MethodBase, Constructor, Parameter, TypeVariable, TypeArgument } = require('java-mti');
|
||||
const { AnyType } = require('./anys');
|
||||
const { Token } = require('./tokenizer');
|
||||
|
||||
/**
|
||||
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser|string} scope_or_package_name
|
||||
* @param {string} name
|
||||
*/
|
||||
function generateShortSignature(scope_or_package_name, name) {
|
||||
if (scope_or_package_name instanceof SourceType) {
|
||||
const type = scope_or_package_name;
|
||||
return `${type._rawShortSignature}$${name}`;
|
||||
}
|
||||
if (scope_or_package_name instanceof SourceMethod
|
||||
|| scope_or_package_name instanceof SourceConstructor
|
||||
|| scope_or_package_name instanceof SourceInitialiser) {
|
||||
const method = scope_or_package_name;
|
||||
return `${method.owner._rawShortSignature}$${method.owner.localTypeCount += 1}${name}`;
|
||||
}
|
||||
const pkgname = scope_or_package_name;
|
||||
return pkgname ?`${pkgname.replace(/\./g, '/')}/${name}` : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} enum_type
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function createImplicitEnumMethods(enum_type, typemap) {
|
||||
return [
|
||||
new class extends Method {
|
||||
constructor() {
|
||||
super(enum_type, 'values', ['public','static'], '');
|
||||
this._returnType = new ArrayType(enum_type, 1);
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
},
|
||||
new class extends Method {
|
||||
constructor() {
|
||||
super(enum_type, 'valueOf', ['public','static'], '');
|
||||
this._parameters = [
|
||||
new Parameter('name', typemap.get('java/lang/String'), false)
|
||||
]
|
||||
this._returnType = enum_type;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class SourceType extends CEIType {
|
||||
/**
|
||||
* @param {string} rawShortSignature
|
||||
* @param {'class'|'interface'|'enum'|'@interface'} typeKind
|
||||
* @param {string[]|number} modifiers
|
||||
* @param {string} docs
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(rawShortSignature, typeKind, modifiers, docs, typemap) {
|
||||
super(rawShortSignature, typeKind, modifiers, docs);
|
||||
/**
|
||||
* Number of local/anonymous types declared in the scope of this type
|
||||
* The number is used when naming them.
|
||||
*/
|
||||
this.localTypeCount = 0;
|
||||
/** @type {SourceTypeIdent[]} */
|
||||
this.extends_types = [];
|
||||
/** @type {SourceTypeIdent[]} */
|
||||
this.implements_types = [];
|
||||
/** @type {SourceConstructor[]} */
|
||||
this.constructors = [];
|
||||
/** @type {Method[]} */
|
||||
this.methods = typeKind === 'enum'
|
||||
? createImplicitEnumMethods(this, typemap)
|
||||
: [];
|
||||
/** @type {SourceField[]} */
|
||||
this.fields = [];
|
||||
/** @type {SourceInitialiser[]} */
|
||||
this.initers = [];
|
||||
/** @type {SourceEnumValue[]} */
|
||||
this.enumValues = [];
|
||||
this.typemap = typemap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} docs
|
||||
* @param {Token} ident
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {SourceType} anonymousType
|
||||
*/
|
||||
addEnumValue(docs, ident, ctr_args, anonymousType) {
|
||||
this.enumValues.push(new SourceEnumValue(this, docs, ident, ctr_args, anonymousType));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceMethod[]}
|
||||
*/
|
||||
get sourceMethods() {
|
||||
// @ts-ignore
|
||||
return this.methods.filter(m => m instanceof SourceMethod);// [...this.implicitMethods, ...this.sourceMethods];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AnonymousSourceType extends SourceType {
|
||||
|
||||
/**
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} scope
|
||||
*/
|
||||
static genSignature(scope) {
|
||||
const type = scope instanceof SourceType ? scope : scope.owner;
|
||||
return `${type._rawShortSignature}$${type.localTypeCount += 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceTypeIdent} typeident
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(typeident, outer_scope, typemap) {
|
||||
super(AnonymousSourceType.genSignature(outer_scope), 'class', [], '', typemap);
|
||||
this.simpleTypeName = typeident.resolved.simpleTypeName;
|
||||
this.typeIdent = typeident;
|
||||
}
|
||||
|
||||
get dottedTypeName() {
|
||||
return this.typeIdent.resolved.dottedTypeName;
|
||||
}
|
||||
|
||||
get fullyDottedRawName() {
|
||||
return this.dottedTypeName;
|
||||
}
|
||||
|
||||
get fullyDottedTypeName() {
|
||||
return this.dottedTypeName;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return `new ${this.dottedTypeName}`;
|
||||
}
|
||||
|
||||
/** @type {JavaType[]} */
|
||||
get supers() {
|
||||
if (this.typeIdent.resolved instanceof AnyType || this.typeIdent.resolved.typeKind !== 'class') {
|
||||
return [this.typemap.get('java/lang/Object')]
|
||||
}
|
||||
return [this.typeIdent.resolved];
|
||||
}
|
||||
|
||||
get shortSignature() {
|
||||
return this._rawShortSignature;
|
||||
}
|
||||
|
||||
get rawTypeSignature() {
|
||||
return `L${this._rawShortSignature};`;
|
||||
}
|
||||
|
||||
get typeSignature() {
|
||||
return this.rawTypeSignature;
|
||||
}
|
||||
}
|
||||
|
||||
class NamedSourceType extends SourceType {
|
||||
/**
|
||||
* @param {string} packageName
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {string} typeKind
|
||||
* @param {Token} kind_token
|
||||
* @param {Token} name_token
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
constructor(packageName, outer_scope, docs, modifiers, typeKind, kind_token, name_token, typemap) {
|
||||
// @ts-ignore
|
||||
super(generateShortSignature(outer_scope || packageName, name_token.value), typeKind, modifiers.map(m => m.source), docs, typemap);
|
||||
super.packageName = packageName;
|
||||
this.modifierTokens = modifiers;
|
||||
this.kind_token = kind_token;
|
||||
this.nameToken = name_token;
|
||||
this.scope = outer_scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} package_name
|
||||
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
|
||||
* @param {string} name
|
||||
*/
|
||||
static getShortSignature(package_name, outer_scope, name) {
|
||||
return generateShortSignature(outer_scope || package_name || '', name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
*/
|
||||
setModifierTokens(mods) {
|
||||
this.modifierTokens = mods;
|
||||
this.modifiers = mods.map(m => m.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType[]} types
|
||||
* @returns {CEIType}
|
||||
*/
|
||||
specialise(types) {
|
||||
const short_sig = `${this.shortSignature}<${types.map(t => t.typeSignature).join('')}>`;
|
||||
if (this.typemap.has(short_sig)) {
|
||||
// @ts-ignore
|
||||
return this.typemap.get(short_sig);
|
||||
}
|
||||
/** @type {'class'|'enum'|'interface'|'@interface'} */
|
||||
// @ts-ignore
|
||||
const typeKind = this.typeKind;
|
||||
const specialised_type = new SpecialisedSourceType(this, typeKind, this._rawShortSignature, types);
|
||||
this.typemap.set(short_sig, specialised_type);
|
||||
return specialised_type;
|
||||
}
|
||||
|
||||
get supers() {
|
||||
const supertypes = [...this.extends_types, ...this.implements_types].map(x => x.resolved);
|
||||
if (this.typeKind === 'enum') {
|
||||
/** @type {CEIType} */
|
||||
const enumtype = this.typemap.get('java/lang/Enum');
|
||||
supertypes.unshift(enumtype.specialise([this]));
|
||||
}
|
||||
else if (!supertypes.find(type => type.typeKind === 'class')) {
|
||||
supertypes.unshift(this.typemap.get('java/lang/Object'));
|
||||
}
|
||||
return supertypes;
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialisedSourceType extends CEIType {
|
||||
/**
|
||||
*
|
||||
* @param {SourceType} source_type
|
||||
* @param {'class'|'enum'|'interface'|'@interface'} typeKind
|
||||
* @param {string} raw_short_signature
|
||||
* @param {JavaType[]} types
|
||||
*/
|
||||
constructor(source_type, typeKind, raw_short_signature, types) {
|
||||
super(raw_short_signature, typeKind, source_type.modifiers, source_type.docs);
|
||||
this.source_type = source_type;
|
||||
this.typemap = source_type.typemap;
|
||||
/** @type {TypeArgument[]} */
|
||||
// @ts-ignore
|
||||
const type_args = source_type.typeVariables.map((tv, idx) => new TypeArgument(this, tv, types[idx] || this.typemap.get('java/lang/Object')));
|
||||
this.typeVariables = type_args;
|
||||
|
||||
function resolveType(type, typevars = []) {
|
||||
if (type instanceof ArrayType) {
|
||||
return new ArrayType(resolveType(type.base, typevars), type.arrdims);
|
||||
}
|
||||
if (!(type instanceof TypeVariableType)) {
|
||||
return type;
|
||||
}
|
||||
if (typevars.includes(type.typeVariable)) {
|
||||
return type;
|
||||
}
|
||||
const specialised_type = type_args.find(ta => ta.name === type.typeVariable.name);
|
||||
return specialised_type.type;
|
||||
}
|
||||
|
||||
this.fields = source_type.fields.map(f => {
|
||||
const type = this;
|
||||
return new class extends Field {
|
||||
constructor() {
|
||||
super(f.modifiers, f.docs);
|
||||
this.owner = type;
|
||||
this.source = f;
|
||||
this.fieldType = resolveType(f.fieldTypeIdent.resolved);
|
||||
}
|
||||
get name() { return this.source.name }
|
||||
get type() { return this.fieldType }
|
||||
};
|
||||
});
|
||||
|
||||
this.constructors = source_type.constructors.map(c => {
|
||||
const type = this;
|
||||
return new class extends Constructor {
|
||||
constructor() {
|
||||
super(type, c.modifiers, c.docs);
|
||||
this.owner = type;
|
||||
this.source = c;
|
||||
this._parameters = c.sourceParameters.map(p => new Parameter(p.name, resolveType(p.paramTypeIdent.resolved, c.typeVariables), p.varargs));
|
||||
}
|
||||
get hasImplementation() {
|
||||
return !!this.source.body;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get typeVariables() {
|
||||
return this.source.typeVars;
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
this.methods = source_type.methods.map(method => {
|
||||
if (!(method instanceof SourceMethod)) {
|
||||
return method;
|
||||
}
|
||||
const m = method;
|
||||
const type = this;
|
||||
return new class extends Method {
|
||||
constructor() {
|
||||
super(type, m.name, m.modifiers, m.docs);
|
||||
this.owner = type;
|
||||
this.source = m;
|
||||
this._returnType = resolveType(m.returnType, m.typeVars)
|
||||
this._parameters = m.sourceParameters.map(p => new Parameter(p.name, resolveType(p.type, m.typeVars), p.varargs));
|
||||
}
|
||||
get hasImplementation() {
|
||||
return !!this.source.body;
|
||||
}
|
||||
get parameters() {
|
||||
return this._parameters;
|
||||
}
|
||||
get returnType() {
|
||||
return this._returnType;
|
||||
}
|
||||
get typeVariables() {
|
||||
return this.source.typeVars;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaType[]} types
|
||||
* @returns {CEIType}
|
||||
*/
|
||||
specialise(types) {
|
||||
const short_sig = `${this._rawShortSignature}<${types.map(t => t.typeSignature).join('')}>`;
|
||||
if (this.typemap.has(short_sig)) {
|
||||
// @ts-ignore
|
||||
return this.typemap.get(short_sig);
|
||||
}
|
||||
/** @type {'class'|'enum'|'interface'|'@interface'} */
|
||||
// @ts-ignore
|
||||
const typeKind = this.typeKind;
|
||||
const specialised_type = new SpecialisedSourceType(this.source_type, typeKind, this._rawShortSignature, types);
|
||||
this.typemap.set(short_sig, specialised_type);
|
||||
return specialised_type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SourceEnumValue extends Field {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token} ident
|
||||
* @param {ResolvedIdent[]} ctr_args
|
||||
* @param {SourceType} anonymousType
|
||||
*/
|
||||
constructor(owner, docs, ident, ctr_args, anonymousType) {
|
||||
super(['public','static','final'], docs);
|
||||
this.owner = owner;
|
||||
this.ident = ident;
|
||||
this.value = ctr_args;
|
||||
this.anonymousType = anonymousType;
|
||||
}
|
||||
|
||||
get label() {
|
||||
// don't include the implicit modifiers in the label
|
||||
return `${this.owner.simpleTypeName} ${this.name}`;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.ident.value;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.owner;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceTypeIdent {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
constructor(tokens, type) {
|
||||
this.tokens = tokens;
|
||||
this.resolved = type;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceField extends Field {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceTypeIdent} field_type_ident
|
||||
* @param {Token} name_token
|
||||
* @param {ResolvedIdent} init
|
||||
*/
|
||||
constructor(owner, docs, modifiers, field_type_ident, name_token, init) {
|
||||
super(modifiers.map(m => m.value), docs);
|
||||
this.owner = owner;
|
||||
this.modifierTokens = modifiers;
|
||||
this.fieldTypeIdent = field_type_ident;
|
||||
this.nameToken = name_token;
|
||||
this.init = init;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.nameToken ? this.nameToken.value : '';
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.fieldTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceConstructor extends Constructor {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {TypeVariable[]} type_vars
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceParameter[]} parameters
|
||||
* @param {JavaType[]} throws
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, type_vars, modifiers, parameters, throws, body_tokens) {
|
||||
super(owner, modifiers.map(m => m.value), docs);
|
||||
this.owner = owner;
|
||||
this.typeVars = type_vars;
|
||||
this.modifierTokens = modifiers;
|
||||
this.sourceParameters = parameters;
|
||||
this.throws = throws;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
get hasImplementation() {
|
||||
return !!this.body;
|
||||
}
|
||||
|
||||
get parameterCount() {
|
||||
return this.sourceParameters.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return this.sourceParameters;
|
||||
}
|
||||
|
||||
get typeVariables() {
|
||||
return this.typeVars;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceMethod extends Method {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {TypeVariable[]} type_vars
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceAnnotation[]} annotations
|
||||
* @param {SourceTypeIdent} method_type_ident
|
||||
* @param {Token} name_token
|
||||
* @param {SourceParameter[]} parameters
|
||||
* @param {JavaType[]} throws
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, type_vars, modifiers, annotations, method_type_ident, name_token, parameters, throws, body_tokens) {
|
||||
super(owner, name_token ? name_token.value : '', modifiers.map(m => m.value), docs);
|
||||
this.annotations = annotations;
|
||||
this.owner = owner;
|
||||
this.typeVars = type_vars;
|
||||
this.modifierTokens = modifiers;
|
||||
this.returnTypeIdent = method_type_ident;
|
||||
this.nameToken = name_token;
|
||||
this.sourceParameters = parameters;
|
||||
this.throws = throws;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
get hasImplementation() {
|
||||
return !!this.body;
|
||||
}
|
||||
|
||||
get parameterCount() {
|
||||
return this.sourceParameters.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return this.sourceParameters;
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return this.returnTypeIdent.resolved;
|
||||
}
|
||||
|
||||
get typeVariables() {
|
||||
return this.typeVars;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceInitialiser extends MethodBase {
|
||||
/**
|
||||
* @param {SourceType} owner
|
||||
* @param {string} docs
|
||||
* @param {Token[]} modifiers
|
||||
* @param {Token[]} body_tokens
|
||||
*/
|
||||
constructor(owner, docs, modifiers, body_tokens) {
|
||||
super(owner, modifiers.map(m => m.value), docs);
|
||||
/** @type {SourceType} */
|
||||
this.owner = owner;
|
||||
this.modifierTokens = modifiers;
|
||||
this.body = {
|
||||
tokens: body_tokens,
|
||||
/** @type {import('./body-types').Local[]} */
|
||||
locals: [],
|
||||
/** @type {import('./statementtypes/Block').Block} */
|
||||
block: null,
|
||||
}
|
||||
this.parsed = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SourceParameter[]}
|
||||
*/
|
||||
get parameters() {
|
||||
return [];
|
||||
}
|
||||
|
||||
get returnType() {
|
||||
return PrimitiveType.map.V;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceParameter extends Parameter {
|
||||
/**
|
||||
* @param {Token[]} modifiers
|
||||
* @param {SourceTypeIdent} typeident
|
||||
* @param {boolean} varargs
|
||||
* @param {Token} name_token
|
||||
*/
|
||||
constructor(modifiers, typeident, varargs, name_token) {
|
||||
super(name_token ? name_token.value : '', typeident.resolved, varargs);
|
||||
this.nameToken = name_token;
|
||||
this.modifierTokens = modifiers;
|
||||
this.paramTypeIdent = typeident;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.paramTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceAnnotation {
|
||||
/**
|
||||
* @param {SourceTypeIdent} typeident
|
||||
*/
|
||||
constructor(typeident) {
|
||||
this.annotationTypeIdent = typeident;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.annotationTypeIdent.resolved;
|
||||
}
|
||||
}
|
||||
|
||||
class SourcePackage {
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(tokens, name) {
|
||||
this.tokens = tokens;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceImport {
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {Token[]} name_tokens
|
||||
* @param {string} pkg_name
|
||||
* @param {Token} static_token
|
||||
* @param {Token} asterisk_token
|
||||
* @param {import('./parsetypes/resolved-import')} resolved
|
||||
*/
|
||||
constructor(tokens, name_tokens, pkg_name, static_token, asterisk_token, resolved) {
|
||||
this.tokens = tokens;
|
||||
this.nameTokens = name_tokens;
|
||||
this.package_name = pkg_name;
|
||||
this.staticToken = static_token;
|
||||
this.asteriskToken = asterisk_token;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
get isDemandLoad() {
|
||||
return !!this.asteriskToken;
|
||||
}
|
||||
|
||||
get isStatic() {
|
||||
return !!this.staticToken;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceUnit {
|
||||
/** @type {string} */
|
||||
uri = '';
|
||||
/** @type {Token[]} */
|
||||
tokens = [];
|
||||
/** @type {SourcePackage} */
|
||||
package_ = null;
|
||||
/** @type {SourceImport[]} */
|
||||
imports = [];
|
||||
/** @type {SourceType[]} */
|
||||
types = [];
|
||||
|
||||
/**
|
||||
* @param {Token} token
|
||||
*/
|
||||
getSourceMethodAtToken(token) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
for (let type of this.types) {
|
||||
for (let method of [...type.sourceMethods, ...type.constructors, ...type.initers]) {
|
||||
if (method.body && method.body.tokens && method.body.tokens.includes(token)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} char_index
|
||||
*/
|
||||
getTokenAt(char_index) {
|
||||
let i = 0;
|
||||
for (let tok of this.tokens) {
|
||||
if (char_index > tok.range.start + tok.range.length) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
while (i > 0 && tok.kind === 'wsc') {
|
||||
tok = this.tokens[--i];
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} char_index
|
||||
*/
|
||||
getCompletionOptionsAt(char_index) {
|
||||
const token = this.getTokenAt(char_index);
|
||||
const method = this.getSourceMethodAtToken(token);
|
||||
// we should also include local variables here, but
|
||||
// it's currently difficult to map an individual token to a scope
|
||||
return {
|
||||
index: char_index,
|
||||
loc: token && token.loc,
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the package this unit belongs to
|
||||
*/
|
||||
get packageName() {
|
||||
return (this.package_ && this.package_.name) || '';
|
||||
}
|
||||
}
|
||||
|
||||
class SourceArrayType extends ArrayType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} element_type
|
||||
*/
|
||||
constructor(element_type) {
|
||||
super(element_type, 1);
|
||||
this.parent_type = element_type;
|
||||
}
|
||||
get label() {
|
||||
return `${this.parent_type.label}[]`;
|
||||
}
|
||||
}
|
||||
|
||||
class FixedLengthArrayType extends SourceArrayType {
|
||||
/**
|
||||
*
|
||||
* @param {JavaType} element_type
|
||||
* @param {ResolvedIdent} length
|
||||
*/
|
||||
constructor(element_type, length) {
|
||||
super(element_type);
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return `${this.parent_type.label}[${this.length.source}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMethodLike
|
||||
*/
|
||||
|
||||
exports.SourceType = SourceType;
|
||||
exports.SourceTypeIdent = SourceTypeIdent;
|
||||
exports.SourceField = SourceField;
|
||||
exports.SourceMethod = SourceMethod;
|
||||
exports.SourceParameter = SourceParameter;
|
||||
exports.SourceConstructor = SourceConstructor;
|
||||
exports.SourceInitialiser = SourceInitialiser;
|
||||
exports.SourceAnnotation = SourceAnnotation;
|
||||
exports.SourceUnit = SourceUnit;
|
||||
exports.SourcePackage = SourcePackage;
|
||||
exports.SourceImport = SourceImport;
|
||||
exports.SourceEnumValue = SourceEnumValue;
|
||||
exports.SourceArrayType = SourceArrayType;
|
||||
exports.FixedLengthArrayType = FixedLengthArrayType;
|
||||
exports.NamedSourceType = NamedSourceType;
|
||||
exports.AnonymousSourceType = AnonymousSourceType;
|
||||
37
langserver/java/statement-validater.js
Normal file
37
langserver/java/statement-validater.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const ParseProblem = require('./parsetypes/parse-problem');
|
||||
|
||||
const { CEIType } = require('java-mti')
|
||||
const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
|
||||
|
||||
const { Block } = require("./statementtypes/Block");
|
||||
const { Statement } = require("./statementtypes/Statement");
|
||||
const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement");
|
||||
|
||||
const { ValidateInfo } = require('./body-types');
|
||||
|
||||
/**
|
||||
* @param {Block} block
|
||||
* @param {SourceMethod | SourceConstructor | SourceInitialiser} method
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {ParseProblem[]} problems
|
||||
*/
|
||||
function checkStatementBlock(block, method, typemap, problems) {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
block.validate(new ValidateInfo(typemap, problems, method));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Statement} statement
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
function checkNonVarDeclStatement(statement, vi) {
|
||||
if (statement instanceof LocalDeclStatement) {
|
||||
vi.problems.push(ParseProblem.Error(statement.locals[0].decltoken, `Local variables cannot be declared as single conditional statements`));
|
||||
};
|
||||
statement.validate(vi);
|
||||
}
|
||||
|
||||
exports.checkStatementBlock = checkStatementBlock;
|
||||
exports.checkNonVarDeclStatement = checkNonVarDeclStatement;
|
||||
39
langserver/java/statementtypes/AssertStatement.js
Normal file
39
langserver/java/statementtypes/AssertStatement.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
|
||||
class AssertStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
message = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.expression) {
|
||||
const value = this.expression.resolveExpression(vi);
|
||||
if (!(value instanceof JavaType) || !isTypeAssignable(PrimitiveType.map.Z, value)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Boolean expression expected`));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.message) {
|
||||
const msg_value = this.message.resolveExpression(vi);
|
||||
if (!(msg_value instanceof JavaType)) {
|
||||
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression expected`));
|
||||
} else if (msg_value === PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression type cannot be 'void'`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.AssertStatement = AssertStatement;
|
||||
46
langserver/java/statementtypes/Block.js
Normal file
46
langserver/java/statementtypes/Block.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').Label} Label
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceType} SourceType
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class Block extends Statement {
|
||||
/** @type {Statement[]} */
|
||||
statements = [];
|
||||
|
||||
/** @type {{locals: Local[], labels: Label[], types: SourceType[]}} */
|
||||
decls = null;
|
||||
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} open
|
||||
*/
|
||||
constructor(owner, open) {
|
||||
super(owner);
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.decls) {
|
||||
const locals = this.decls.locals.reverse();
|
||||
locals.forEach(local => {
|
||||
if (locals.find(l => l.name === local.name) !== local) {
|
||||
vi.problems.push(ParseProblem.Error(local.decltoken, `Variable redeclared: ${local.name}`))
|
||||
}
|
||||
});
|
||||
}
|
||||
for (let statement of this.statements) {
|
||||
statement.validate(vi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Block = Block;
|
||||
23
langserver/java/statementtypes/BreakStatement.js
Normal file
23
langserver/java/statementtypes/BreakStatement.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class BreakStatement extends KeywordStatement {
|
||||
/** @type {Token} */
|
||||
target = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!vi.statementStack.find(s => /^(for|do|while|switch)$/.test(s))) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `break can only be specified inside loop/switch statements`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.BreakStatement = BreakStatement;
|
||||
23
langserver/java/statementtypes/ContinueStatement.js
Normal file
23
langserver/java/statementtypes/ContinueStatement.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ContinueStatement extends KeywordStatement {
|
||||
/** @type {Token} */
|
||||
target = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!vi.statementStack.find(s => /^(for|do|while)$/.test(s))) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `continue can only be specified inside loop statements`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ContinueStatement = ContinueStatement;
|
||||
31
langserver/java/statementtypes/DoStatement.js
Normal file
31
langserver/java/statementtypes/DoStatement.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('../statementtypes/Block').Block} Block
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
|
||||
class DoStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Block} */
|
||||
block = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.block) {
|
||||
vi.statementStack.unshift('do');
|
||||
this.block.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
}
|
||||
|
||||
exports.DoStatement = DoStatement;
|
||||
6
langserver/java/statementtypes/EmptyStatement.js
Normal file
6
langserver/java/statementtypes/EmptyStatement.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
class EmptyStatement extends Statement {
|
||||
}
|
||||
|
||||
exports.EmptyStatement = EmptyStatement;
|
||||
42
langserver/java/statementtypes/ExpressionStatement.js
Normal file
42
langserver/java/statementtypes/ExpressionStatement.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../expressiontypes/Expression').Expression} Expression
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const { BinaryOpExpression } = require('../expressiontypes/BinaryOpExpression');
|
||||
const { MethodCallExpression } = require('../expressiontypes/MethodCallExpression');
|
||||
const { NewObject } = require('../expressiontypes/NewExpression');
|
||||
const { IncDecExpression } = require('../expressiontypes/IncDecExpression');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ExpressionStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {ResolvedIdent} expression
|
||||
*/
|
||||
constructor(owner, expression) {
|
||||
super(owner);
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
// only method calls, new objects, increments and assignments are allowed as expression statements
|
||||
const e = this.expression.variables[0];
|
||||
let is_statement = e instanceof MethodCallExpression || e instanceof NewObject || e instanceof IncDecExpression;
|
||||
if (e instanceof BinaryOpExpression) {
|
||||
is_statement = e.op.kind === 'assignment-operator';
|
||||
}
|
||||
if (!is_statement) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Statement expected`));
|
||||
}
|
||||
this.expression.resolveExpression(vi);
|
||||
}
|
||||
}
|
||||
|
||||
exports.ExpressionStatement = ExpressionStatement;
|
||||
55
langserver/java/statementtypes/ForStatement.js
Normal file
55
langserver/java/statementtypes/ForStatement.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkNonVarDeclStatement } = require('../statement-validater');
|
||||
const { Local, ResolvedIdent } = require('../body-types');
|
||||
|
||||
class ForStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent[] | Local[]} */
|
||||
init = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {ResolvedIdent[]} */
|
||||
update = null;
|
||||
/** @type {ResolvedIdent} */
|
||||
iterable = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.init) {
|
||||
this.init.forEach(x => {
|
||||
if (x instanceof ResolvedIdent) {
|
||||
x.resolveExpression(vi);
|
||||
} else if (x instanceof Local) {
|
||||
if (x.init) {
|
||||
x.init.resolveExpression(vi);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.test) {
|
||||
this.test.resolveExpression(vi);
|
||||
}
|
||||
if (this.update) {
|
||||
this.update.forEach(e => e.resolveExpression(vi));
|
||||
}
|
||||
if (this.iterable) {
|
||||
this.iterable.resolveExpression(vi);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('for');
|
||||
checkNonVarDeclStatement(this.statement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ForStatement = ForStatement;
|
||||
39
langserver/java/statementtypes/IfStatement.js
Normal file
39
langserver/java/statementtypes/IfStatement.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
const { checkNonVarDeclStatement } = require('../statement-validater');
|
||||
|
||||
class IfStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
/** @type {Statement} */
|
||||
elseStatement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.test) {
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('if');
|
||||
checkNonVarDeclStatement(this.statement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
if (this.elseStatement) {
|
||||
vi.statementStack.unshift('else');
|
||||
checkNonVarDeclStatement(this.elseStatement, vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.IfStatement = IfStatement;
|
||||
18
langserver/java/statementtypes/InvalidStatement.js
Normal file
18
langserver/java/statementtypes/InvalidStatement.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
class InvalidStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} token
|
||||
*/
|
||||
constructor(owner, token) {
|
||||
super(owner);
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
exports.InvalidStatement = InvalidStatement;
|
||||
22
langserver/java/statementtypes/KeywordStatement.js
Normal file
22
langserver/java/statementtypes/KeywordStatement.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
|
||||
/**
|
||||
* A statement that begins with a keyword (if, do, while, etc)
|
||||
*/
|
||||
class KeywordStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Token} keyword
|
||||
*/
|
||||
constructor(owner, keyword) {
|
||||
super(owner);
|
||||
this.keyword = keyword;
|
||||
}
|
||||
}
|
||||
|
||||
exports.KeywordStatement = KeywordStatement;
|
||||
35
langserver/java/statementtypes/LocalDeclStatement.js
Normal file
35
langserver/java/statementtypes/LocalDeclStatement.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
* @typedef {import('../body-types').Label} Label
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../source-types').SourceType} SourceType
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
const { Statement } = require("./Statement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { checkAssignment } = require('../expression-resolver');
|
||||
|
||||
class LocalDeclStatement extends Statement {
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
* @param {Local[]} locals
|
||||
*/
|
||||
constructor(owner, locals) {
|
||||
super(owner);
|
||||
this.locals = locals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
this.locals.forEach(local => {
|
||||
if (local.init) {
|
||||
checkAssignment(vi, local.type, local.init);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.LocalDeclStatement = LocalDeclStatement;
|
||||
61
langserver/java/statementtypes/ReturnStatement.js
Normal file
61
langserver/java/statementtypes/ReturnStatement.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
const { LambdaType, MultiValueType } = require('../anys');
|
||||
|
||||
class ReturnStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
const method_return_type = vi.method.returnType;
|
||||
if (!this.expression) {
|
||||
if (method_return_type !== PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.keyword, `Method must return a value of type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method_return_type === PrimitiveType.map.V) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `void method cannot return a value`));
|
||||
return;
|
||||
}
|
||||
const type = this.expression.resolveExpression(vi);
|
||||
checkType(type, () => this.expression.tokens);
|
||||
|
||||
/**
|
||||
* @param {ResolvedValue} type
|
||||
* @param {() => Token[]} tokens
|
||||
*/
|
||||
function checkType(type, tokens) {
|
||||
if (type instanceof JavaType || type instanceof NumberLiteral) {
|
||||
if (!isTypeAssignable(method_return_type, type)) {
|
||||
const expr_type = type instanceof NumberLiteral ? type.type : type;
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: expression of type '${expr_type.fullyDottedTypeName}' cannot be returned from a method of type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else if (type instanceof MultiValueType) {
|
||||
// ternary, eg. return x > 0 ? 1 : 2;
|
||||
type.types.forEach(type => checkType(type, tokens));
|
||||
} else if (type instanceof LambdaType) {
|
||||
if (!isTypeAssignable(method_return_type, type)) {
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: lambda expression is not compatible with method type '${method_return_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(tokens(), `'${method_return_type.fullyDottedTypeName}' type expression expected`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ReturnStatement = ReturnStatement;
|
||||
17
langserver/java/statementtypes/Statement.js
Normal file
17
langserver/java/statementtypes/Statement.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
|
||||
*/
|
||||
|
||||
class Statement {
|
||||
|
||||
/**
|
||||
* @param {SourceMethodLike} owner
|
||||
*/
|
||||
constructor(owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
validate(vi) {}
|
||||
}
|
||||
|
||||
exports.Statement = Statement;
|
||||
70
langserver/java/statementtypes/SwitchStatement.js
Normal file
70
langserver/java/statementtypes/SwitchStatement.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('../tokenizer').Token} Token
|
||||
*/
|
||||
const { JavaType, PrimitiveType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const { NumberLiteral } = require('../expressiontypes/literals/Number');
|
||||
|
||||
class SwitchStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {(ResolvedIdent|boolean)[]} */
|
||||
cases = [];
|
||||
/** @type {{cases: (ResolvedIdent|boolean)[], statements: Statement[]} []} */
|
||||
caseBlocks = [];
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
let test_type = null;
|
||||
if (this.test) {
|
||||
test_type = this.test.resolveExpression(vi);
|
||||
if (test_type instanceof NumberLiteral) {
|
||||
test_type = test_type.type;
|
||||
}
|
||||
if (test_type instanceof JavaType) {
|
||||
if (!isTypeAssignable(vi.typemap.get('java/lang/String'), test_type)) {
|
||||
if (!isTypeAssignable(PrimitiveType.map.I, test_type)) {
|
||||
test_type = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
test_type = null;
|
||||
}
|
||||
if (!test_type) {
|
||||
vi.problems.push(ParseProblem.Error(this.test.tokens, `Switch expression must be of type 'int' or 'java.lang.String'`));
|
||||
}
|
||||
}
|
||||
|
||||
vi.statementStack.unshift('switch');
|
||||
|
||||
this.caseBlocks.forEach(caseblock => {
|
||||
caseblock.cases.forEach(c => {
|
||||
if (typeof c === 'boolean') {
|
||||
// default case
|
||||
return;
|
||||
}
|
||||
const case_value = c.resolveExpression(vi);
|
||||
if (case_value instanceof JavaType || case_value instanceof NumberLiteral) {
|
||||
if (test_type && !isTypeAssignable(test_type, case_value)) {
|
||||
const case_type = case_value instanceof JavaType ? case_value : case_value.type;
|
||||
vi.problems.push(ParseProblem.Error(c.tokens, `Incomparable types: expression of type '${case_type.fullyDottedTypeName}' is not comparable with type '${test_type.fullyDottedTypeName}'`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(c.tokens, `Expression expected`));
|
||||
}
|
||||
});
|
||||
caseblock.statements.forEach(statement => statement.validate(vi));
|
||||
})
|
||||
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
exports.SwitchStatement = SwitchStatement;
|
||||
35
langserver/java/statementtypes/SynchronizedStatement.js
Normal file
35
langserver/java/statementtypes/SynchronizedStatement.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { CEIType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class SynchronizedStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.expression) {
|
||||
const value = this.expression.resolveExpression(vi);
|
||||
// locks must be a reference type
|
||||
if (!(value instanceof CEIType)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Lock expression must be a reference type`));
|
||||
}
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('synchronized');
|
||||
this.statement.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.SynchronizedStatement = SynchronizedStatement;
|
||||
32
langserver/java/statementtypes/ThrowStatement.js
Normal file
32
langserver/java/statementtypes/ThrowStatement.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { JavaType } = require('java-mti');
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { isTypeAssignable } = require('../expression-resolver');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
class ThrowStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
expression = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (!this.expression) {
|
||||
return;
|
||||
}
|
||||
const throw_value = this.expression.resolveExpression(vi);
|
||||
if (throw_value instanceof JavaType) {
|
||||
if (!isTypeAssignable(vi.typemap.get('java/lang/Throwable'), throw_value)) {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `throw expression does not inherit from java.lang.Throwable`));
|
||||
}
|
||||
} else {
|
||||
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Throwable expression expected`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ThrowStatement = ThrowStatement;
|
||||
45
langserver/java/statementtypes/TryStatement.js
Normal file
45
langserver/java/statementtypes/TryStatement.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
* @typedef {import('./Block').Block} Block
|
||||
* @typedef {import('../body-types').Local} Local
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { ResolvedIdent } = require('../body-types');
|
||||
const { Block } = require('./Block');
|
||||
|
||||
class TryStatement extends KeywordStatement {
|
||||
/** @type {(ResolvedIdent|Local[])[]} */
|
||||
resources = [];
|
||||
/** @type {Block} */
|
||||
block = null;
|
||||
catches = [];
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
this.resources.forEach(r => {
|
||||
if (r instanceof ResolvedIdent) {
|
||||
r.resolveExpression(vi);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.block) {
|
||||
vi.statementStack.unshift('try');
|
||||
this.block.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
|
||||
this.catches.forEach(c => {
|
||||
if (c instanceof Block) {
|
||||
// finally
|
||||
c.validate(vi);
|
||||
} else if (c.block) {
|
||||
// catch block
|
||||
c.block.validate(vi);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.TryStatement = TryStatement;
|
||||
31
langserver/java/statementtypes/WhileStatement.js
Normal file
31
langserver/java/statementtypes/WhileStatement.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('./Statement').Statement} Statement
|
||||
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
|
||||
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
|
||||
*/
|
||||
const { KeywordStatement } = require("./KeywordStatement");
|
||||
const { checkBooleanBranchCondition } = require('../expression-resolver');
|
||||
|
||||
class WhileStatement extends KeywordStatement {
|
||||
/** @type {ResolvedIdent} */
|
||||
test = null;
|
||||
/** @type {Statement} */
|
||||
statement = null;
|
||||
|
||||
/**
|
||||
* @param {ValidateInfo} vi
|
||||
*/
|
||||
validate(vi) {
|
||||
if (this.test) {
|
||||
const value = this.test.resolveExpression(vi);
|
||||
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
|
||||
}
|
||||
if (this.statement) {
|
||||
vi.statementStack.unshift('while');
|
||||
this.statement.validate(vi);
|
||||
vi.statementStack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.WhileStatement = WhileStatement;
|
||||
269
langserver/java/tokenizer.js
Normal file
269
langserver/java/tokenizer.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @typedef {import('java-mti').Method} Method
|
||||
* @typedef {import('java-mti').Constructor} Constructor
|
||||
*/
|
||||
const { TextBlock, BlockRange } = require('./parsetypes/textblock');
|
||||
|
||||
/**
|
||||
* Convert a token to its simplified form for easier declaration parsing.
|
||||
*
|
||||
* - Whitespace, comments, strings and character literals are normalised.
|
||||
* - Modifier keywords and identifers are abbreviated.
|
||||
* - Any invalid text is replaced with spaces.
|
||||
*
|
||||
* Abbreviated and normalised values are padded to occupy the same space
|
||||
* as the original text - this ensures any parse errors are reported in the
|
||||
* correct location.
|
||||
* @param {string} text
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
* @param {string} kind
|
||||
*/
|
||||
function tokenKindToSimplified(text, start, length, kind) {
|
||||
const chunk = text.slice(start, start + length);
|
||||
switch (kind) {
|
||||
case 'wsc':
|
||||
return chunk.replace(/[^\r\n]/g, ' ');
|
||||
case 'string-literal':
|
||||
if (chunk.length <= 2) return chunk;
|
||||
return `"${'#'.repeat(chunk.length - 2)}"`;
|
||||
case 'char-literal':
|
||||
if (chunk.length <= 2) return chunk;
|
||||
return `'${'#'.repeat(chunk.length - 2)}'`;
|
||||
case 'primitive-type':
|
||||
return `P${' '.repeat(chunk.length - 1)}`;
|
||||
case 'modifier':
|
||||
return `M${' '.repeat(chunk.length - 1)}`;
|
||||
case 'ident':
|
||||
return `W${' '.repeat(chunk.length - 1)}`;
|
||||
case 'invalid':
|
||||
return ' '.repeat(chunk.length);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
class Token extends TextBlock {
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {number} start
|
||||
* @param {number} length
|
||||
* @param {string} kind
|
||||
*/
|
||||
constructor(text, start, length, kind) {
|
||||
super(new BlockRange(text, start, length), tokenKindToSimplified(text, start, length, kind));
|
||||
this.kind = kind;
|
||||
/** @type {{key:string}} */
|
||||
this.loc = null;
|
||||
|
||||
/**
|
||||
* Stores information about the resolved methods/constructors this token is an argument for.
|
||||
* This is used to provide method signature info to vscode
|
||||
* @type {{methods:(Method|Constructor)[], methodIdx:number, argIdx:number}}
|
||||
*/
|
||||
this.methodCallInfo = null;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.source;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* \s+ whitespace
|
||||
* \/\/.* single-line comment (slc)
|
||||
* \/\*[\d\D]*?\*\/ multi-line comment (mlc)
|
||||
* "[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*" string literal - correctly terminated but may contain invalid escapes
|
||||
* ".* unterminated string literal
|
||||
* '\\?.?'? character literal - possibly unterminated and/or with invalid escape
|
||||
* \.?\d number literal (start) - further processing extracts the value
|
||||
* [\p{L}\p{N}_$]* word - keyword or identifier
|
||||
* [;,?:(){}\[\]] single-character symbols and operators
|
||||
* \.(\.\.)? . ...
|
||||
*
|
||||
* the operators: [!=/%*^]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(=|\++)?|\-+=?
|
||||
* [!=/%*^]=? ! = / % * ^ != == /= %= *= ^=
|
||||
* <<?=? < << <= <<=
|
||||
* >>?[>=]? > >> >= >>> >>=
|
||||
* &[&=]? & && &=
|
||||
* \|[|=]? | || |=
|
||||
* (\+\+|--) ++ -- postfix inc - only matches if immediately preceded by a word or a ]
|
||||
* [+-]=? + - += -=
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {number} [offset]
|
||||
* @param {number} [length]
|
||||
*/
|
||||
function tokenize(source, offset = 0, length = source.length) {
|
||||
const text = source.slice(offset, offset + length);
|
||||
const raw_token_re = /(\s+|\/\/.*|\/\*[\d\D]*?\*\/|\/\*[\d\D]*)|("[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*"|".*)|('\\u[\da-fA-F]{0,4}'?|'\\?.?'?)|(\.?\d)|([\p{L}\p{N}$_]+)|(\()|([;,?:(){}\[\]@]|\.(?:\.\.)?)|([!=/%*^]=?|<<?=?|>>?>?=?|&[&=]?|\|[|=]?|(\+\+|--)|->|[+-]=?|~)|$/gu;
|
||||
const raw_token_types = [
|
||||
'wsc',
|
||||
'string-literal',
|
||||
'char-literal',
|
||||
'number-literal',
|
||||
'word',
|
||||
'open-bracket',
|
||||
'symbol',
|
||||
'operator',
|
||||
];
|
||||
/**
|
||||
* Note that some keywords have context-dependant meanings:
|
||||
* default - modifier or statement-keyword
|
||||
* synchronized - modifier or statement-keyword
|
||||
* They are treated as modifiers and updated with their new token-type when method bodies are parsed
|
||||
*
|
||||
* ```
|
||||
* true|false boolean
|
||||
* this|null object
|
||||
* int|long|short|byte|float|double|char|boolean|void primitive type
|
||||
* new
|
||||
* instanceof
|
||||
* public|private|protected|static|final|abstract|native|volatile|transient|default|synchronized modifier
|
||||
* if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw statement keyword
|
||||
* class|enum|interface type keyword
|
||||
* package|import package keyword
|
||||
* \w+ word
|
||||
* ```
|
||||
*/
|
||||
const word_re = /^(?:(true|false)|(this|super|null)|(int|long|short|byte|float|double|char|boolean|void)|(new)|(instanceof)|(public|private|protected|static|final|abstract|native|volatile|transient|strictfp|default|synchronized)|(if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw|assert)|(class|enum|interface)|(extends|implements|throws)|(package|import)|(.+))$/;
|
||||
|
||||
const word_token_types = [
|
||||
'boolean-literal',
|
||||
'object-literal',
|
||||
'primitive-type',
|
||||
'new-operator',
|
||||
'instanceof-operator',
|
||||
'modifier',
|
||||
'statement-kw',
|
||||
'type-kw',
|
||||
'package-kw',
|
||||
'eit-kw',
|
||||
'ident'
|
||||
]
|
||||
/**
|
||||
* ```
|
||||
* \d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]? decimal exponent: 1e0, 1.5e+10, 0.123E-20d
|
||||
* (?:\d+\.\d*|\.\d+)[fFdD]? decimal number: 0.1, 12.34f, 7.D, .3
|
||||
* 0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]? hex exponent: 0x123.abcP-100
|
||||
* 0x[\da-fA-F]*[lL]? hex integer: 0x1, 0xaBc, 0x, 0x7L
|
||||
* \d+[fFdDlL]? integer: 0, 123, 234f, 345L
|
||||
* ```
|
||||
* todo - underscore seperators
|
||||
*/
|
||||
const number_re = /((?:\d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]?)|((?:\d+\.\d*|\.\d+)[fFdD]?)|(0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]?)|(0[xX][\da-fA-F]*[lL]?)|(\d+[fFdDlL]?)/g;
|
||||
const number_token_types = [
|
||||
'dec-exp-number-literal',
|
||||
'dec-number-literal',
|
||||
'hex-exp-number-literal',
|
||||
'hex-number-literal',
|
||||
'int-number-literal',
|
||||
]
|
||||
const tokens = [];
|
||||
let lastindex = 0, m;
|
||||
while (m = raw_token_re.exec(text)) {
|
||||
// any text appearing between two matches is invalid
|
||||
if (m.index > lastindex) {
|
||||
tokens.push(new Token(source, offset + lastindex, m.index - lastindex, 'invalid'));
|
||||
}
|
||||
lastindex = m.index + m[0].length;
|
||||
if (m.index >= text.length) {
|
||||
// end of input
|
||||
break;
|
||||
}
|
||||
|
||||
let idx = m.findIndex((match,i) => i && match) - 1;
|
||||
let tokentype = raw_token_types[idx];
|
||||
|
||||
switch(tokentype) {
|
||||
case 'number-literal':
|
||||
// we need to extract the exact number part
|
||||
number_re.lastIndex = m.index;
|
||||
m = number_re.exec(text);
|
||||
idx = m.findIndex((match,i) => i && match) - 1;
|
||||
tokentype = number_token_types[idx];
|
||||
// update the raw_token_re position based on the length of the extracted number
|
||||
raw_token_re.lastIndex = lastindex = number_re.lastIndex;
|
||||
break;
|
||||
case 'word':
|
||||
// we need to work out what kind of keyword, literal or ident this is
|
||||
let word_m = m[0].match(word_re);
|
||||
idx = word_m.findIndex((match,i) => i && match) - 1;
|
||||
tokentype = word_token_types[idx];
|
||||
break;
|
||||
case 'operator':
|
||||
// find the operator-type
|
||||
tokentype = getOperatorType(m[0]);
|
||||
break;
|
||||
}
|
||||
tokens.push(new Token(source, offset + m.index, m[0].length, tokentype));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ```
|
||||
* =|[/%*&|^+-]=|>>>?=|<<= assignment
|
||||
* \+\+|-- inc
|
||||
* [!=]= equality
|
||||
* [<>]=? comparison
|
||||
* [&|^] bitwise
|
||||
* <<|>>>? shift
|
||||
* &&|[|][|] logical
|
||||
* [*%/] muldiv
|
||||
* [+-] plumin
|
||||
* [~!] unary
|
||||
* ```
|
||||
*/
|
||||
const operator_re = /^(?:(=|[/%*&|^+-]=|>>>?=|<<=)|(\+\+|--)|([!=]=)|([<>]=?)|([&|^])|(<<|>>>?)|(&&|[|][|])|([*%/])|(->)|([+-])|([~!]))$/;
|
||||
/**
|
||||
* @typedef {
|
||||
'assignment-operator'|
|
||||
'inc-operator'|
|
||||
'equality-operator'|
|
||||
'comparison-operator'|
|
||||
'bitwise-operator'|
|
||||
'shift-operator'|
|
||||
'logical-operator'|
|
||||
'muldiv-operator'|
|
||||
'lambda-operator'|
|
||||
'plumin-operator'|
|
||||
'unary-operator'} OperatorKind
|
||||
*/
|
||||
/** @type {OperatorKind[]} */
|
||||
const operator_token_types = [
|
||||
'assignment-operator',
|
||||
'inc-operator',
|
||||
'equality-operator',
|
||||
'comparison-operator',
|
||||
'bitwise-operator',
|
||||
'shift-operator',
|
||||
'logical-operator',
|
||||
'muldiv-operator',
|
||||
'lambda-operator',
|
||||
'plumin-operator',
|
||||
'unary-operator',
|
||||
]
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function getOperatorType(value) {
|
||||
const op_match = value.match(operator_re);
|
||||
const idx = op_match.findIndex((match,i) => i && match) - 1;
|
||||
// @ts-ignore
|
||||
return operator_token_types[idx];
|
||||
}
|
||||
|
||||
|
||||
exports.getOperatorType = getOperatorType;
|
||||
exports.tokenize = tokenize;
|
||||
exports.Token = Token;
|
||||
152
langserver/java/type-resolver.js
Normal file
152
langserver/java/type-resolver.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @typedef {Map<string,CEIType>} TypeMap
|
||||
*/
|
||||
const { JavaType, CEIType, MethodBase, TypeVariable } = require('java-mti');
|
||||
const { ResolvedImport } = require('./import-resolver');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} ident
|
||||
* @param {TypeVariable[]} type_variables
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveTypeOrPackage(ident, type_variables, scope, imports, typemap) {
|
||||
const types = [];
|
||||
let package_name = '';
|
||||
|
||||
const tv = type_variables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
|
||||
if (scope) {
|
||||
|
||||
if (!types[0] && scope instanceof MethodBase) {
|
||||
// is it a type variable in the current scope
|
||||
const tv = scope.typeVariables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
}
|
||||
|
||||
const scoped_type = scope instanceof CEIType ? scope : scope.owner;
|
||||
if (!types[0]) {
|
||||
// is it an enclosed type of the currently scoped type or any outer type
|
||||
const scopes = scoped_type.shortSignature.split('$');
|
||||
while (scopes.length) {
|
||||
const enc_type = typemap.get(`${scopes.join('$')}$${ident}`);
|
||||
if (enc_type) {
|
||||
types.push(enc_type);
|
||||
break;
|
||||
}
|
||||
scopes.pop();
|
||||
}
|
||||
if (!types[0] && scoped_type.simpleTypeName === ident) {
|
||||
types.push(scoped_type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!types[0]) {
|
||||
// is it a type variable of the currently scoped type
|
||||
const tv = scoped_type.typeVariables.find(tv => tv.name === ident);
|
||||
if (tv) {
|
||||
types.push(tv.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!types[0]) {
|
||||
// is it a type from the imports
|
||||
for (let i of imports) {
|
||||
const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(ident) && /[$/]/.test(fqn[fqn.length-ident.length-1]));
|
||||
if (fqn) {
|
||||
types.push(i.types.get(fqn));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!types[0]) {
|
||||
// is it a default-package type
|
||||
const default_type = typemap.get(ident);
|
||||
if (default_type) {
|
||||
types.push(default_type);
|
||||
}
|
||||
}
|
||||
|
||||
// the final option is the start of a package name
|
||||
const package_root = ident + '/';
|
||||
const typelist = [...typemap.keys()];
|
||||
if (typelist.find(fqn => fqn.startsWith(package_root))) {
|
||||
package_name = ident;
|
||||
}
|
||||
|
||||
return {
|
||||
types,
|
||||
package_name,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} ident
|
||||
* @param {JavaType[]} outer_types
|
||||
* @param {string} outer_package_name
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveNextTypeOrPackage(ident, outer_types, outer_package_name, typemap) {
|
||||
const types = [];
|
||||
let package_name = '';
|
||||
|
||||
outer_types.forEach(type => {
|
||||
if (type instanceof CEIType) {
|
||||
const enclosed_type_signature = `${type.shortSignature}$${ident}`;
|
||||
const enclosed_type = typemap.get(enclosed_type_signature);
|
||||
if (enclosed_type) {
|
||||
// it matches an inner/enclosed type
|
||||
types.push(enclosed_type);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (outer_package_name) {
|
||||
const { type, sub_package_name } = resolveNextPackage(outer_package_name, ident, typemap);
|
||||
if (type) {
|
||||
types.push(type);
|
||||
}
|
||||
package_name = sub_package_name;
|
||||
}
|
||||
|
||||
return {
|
||||
types,
|
||||
package_name,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} package_name
|
||||
* @param {string} ident
|
||||
* @param {TypeMap} typemap
|
||||
*/
|
||||
function resolveNextPackage(package_name, ident, typemap) {
|
||||
let type = null, sub_package_name = '';
|
||||
const qualified_name = `${package_name}/${ident}`;
|
||||
type = typemap.get(qualified_name) || null;
|
||||
const package_match = qualified_name + '/';
|
||||
if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) {
|
||||
// it matches a sub-package
|
||||
sub_package_name = qualified_name;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
sub_package_name
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveTypeOrPackage,
|
||||
resolveNextTypeOrPackage,
|
||||
resolveNextPackage,
|
||||
}
|
||||
169
langserver/java/typeident.js
Normal file
169
langserver/java/typeident.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const { ArrayType, CEIType, JavaType, PrimitiveType, MethodBase, WildcardType, TypeVariable } = require('java-mti');
|
||||
const { SourceTypeIdent, SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
|
||||
const ResolvedImport = require('./parsetypes/resolved-import');
|
||||
const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver');
|
||||
const { Token } = require('./tokenizer');
|
||||
const { AnyType } = require("./anys");
|
||||
|
||||
/**
|
||||
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMC
|
||||
* @typedef {import('./TokenList').TokenList} TokenList
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function typeIdentList(tokens, scope, imports, typemap) {
|
||||
let type = typeIdent(tokens, scope, imports, typemap);
|
||||
const types = [type];
|
||||
while (tokens.current.value === ',') {
|
||||
tokens.inc();
|
||||
type = typeIdent(tokens, scope, imports, typemap);
|
||||
types.push(type);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {{no_array_qualifiers:boolean, type_vars:TypeVariable[]}} [opts]
|
||||
*/
|
||||
function typeIdent(tokens, scope, imports, typemap, opts) {
|
||||
tokens.mark();
|
||||
const type = singleTypeIdent(tokens, scope, imports, typemap, opts);
|
||||
return new SourceTypeIdent(tokens.markEnd(), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @param {{no_array_qualifiers:boolean, type_vars: TypeVariable[]}} [opts]
|
||||
*/
|
||||
function singleTypeIdent(tokens, scope, imports, typemap, opts) {
|
||||
/** @type {JavaType[]} */
|
||||
let types = [], package_name = '';
|
||||
tokens.mark();
|
||||
switch(tokens.current.kind) {
|
||||
case 'ident':
|
||||
({ types, package_name } = resolveTypeOrPackage(tokens.current.value, opts ? opts.type_vars : [], scope, imports, typemap));
|
||||
break;
|
||||
case 'primitive-type':
|
||||
types.push(PrimitiveType.fromName(tokens.current.value));
|
||||
break;
|
||||
default:
|
||||
return tokens.current.value === '?'
|
||||
? wildcardTypeArgument(tokens, scope, imports, typemap)
|
||||
: AnyType.Instance;
|
||||
}
|
||||
tokens.inc();
|
||||
for (;;) {
|
||||
if (tokens.isValue('.')) {
|
||||
if (tokens.current.kind !== 'ident') {
|
||||
break;
|
||||
}
|
||||
({ types, package_name } = resolveNextTypeOrPackage(tokens.current.value, types, package_name, typemap));
|
||||
tokens.inc();
|
||||
} else if (tokens.isValue('<')) {
|
||||
genericTypeArgs(tokens, types, scope, imports, typemap);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const type_tokens = tokens.markEnd();
|
||||
if (!types[0]) {
|
||||
const anytype = new AnyType(type_tokens.map(t => t.source).join(''));
|
||||
types.push(anytype);
|
||||
}
|
||||
|
||||
// allow array qualifiers unless specifically disabled
|
||||
const allow_array_qualifiers = !opts || !opts.no_array_qualifiers;
|
||||
if ( allow_array_qualifiers && tokens.isValue('[')) {
|
||||
let arrdims = 0;
|
||||
for(;;) {
|
||||
arrdims++;
|
||||
tokens.expectValue(']');
|
||||
if (!tokens.isValue('[')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
types = types.map(t => new ArrayType(t, arrdims));
|
||||
}
|
||||
|
||||
return types[0];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TokenList} tokens
|
||||
* @param {JavaType[]} types
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
*/
|
||||
function genericTypeArgs(tokens, types, scope, imports, typemap) {
|
||||
if (tokens.isValue('>')) {
|
||||
// <> operator - build new types with inferred type arguments
|
||||
types.forEach((t,i,arr) => {
|
||||
if (t instanceof CEIType) {
|
||||
let specialised = t.makeInferredTypeArgs();
|
||||
arr[i] = specialised;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
const type_arguments = typeIdentList(tokens, scope, imports, typemap).map(s => s.resolved);
|
||||
types.forEach((t,i,arr) => {
|
||||
if (t instanceof CEIType) {
|
||||
let specialised = t.specialise(type_arguments);
|
||||
if (typemap.has(specialised.shortSignature)) {
|
||||
arr[i] = typemap.get(specialised.shortSignature);
|
||||
return;
|
||||
}
|
||||
typemap.set(specialised.shortSignature, specialised);
|
||||
arr[i] = specialised;
|
||||
}
|
||||
});
|
||||
if (/>>>?/.test(tokens.current.value)) {
|
||||
// we need to split >> and >>> into separate > tokens to handle things like List<Class<?>>
|
||||
const new_tokens = tokens.current.value.split('').map((gt,i) => new Token(tokens.current.range.source, tokens.current.range.start + i, 1, 'comparison-operator'));
|
||||
tokens.splice(tokens.idx, 1, ...new_tokens);
|
||||
}
|
||||
tokens.expectValue('>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenList} tokens
|
||||
* @param {CEIType|MethodBase} scope
|
||||
* @param {ResolvedImport[]} imports
|
||||
* @param {Map<string,CEIType>} typemap
|
||||
* @returns {WildcardType}
|
||||
*/
|
||||
function wildcardTypeArgument(tokens, scope, imports, typemap) {
|
||||
tokens.expectValue('?');
|
||||
let bound = null;
|
||||
switch (tokens.current.value) {
|
||||
case 'extends':
|
||||
case 'super':
|
||||
const kind = tokens.current.value;
|
||||
tokens.inc();
|
||||
bound = {
|
||||
kind,
|
||||
type: singleTypeIdent(tokens, scope, imports, typemap),
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new WildcardType(bound);
|
||||
}
|
||||
|
||||
exports.typeIdent = typeIdent;
|
||||
exports.typeIdentList = typeIdentList;
|
||||
exports.genericTypeArgs = genericTypeArgs;
|
||||
65
langserver/java/validater.js
Normal file
65
langserver/java/validater.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { CEIType } = require('java-mti');
|
||||
const { resolveImports } = require('../java/import-resolver');
|
||||
const { SourceUnit } = require('./source-types');
|
||||
const { parseTypeMethods } = require('./body-parser');
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
* @param {Map<string, CEIType>} typemap
|
||||
*/
|
||||
function parseMethodBodies(unit, typemap) {
|
||||
const resolved_types = [
|
||||
...resolveImports(typemap, unit.packageName),
|
||||
...unit.imports.filter(i => i.resolved).map(i => i.resolved),
|
||||
]
|
||||
unit.types.forEach(t => parseTypeMethods(t, resolved_types, typemap));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
* @param {Map<string, CEIType>} androidLibrary
|
||||
* @returns {import('./parsetypes/parse-problem')[]}
|
||||
*/
|
||||
function validate(unit, androidLibrary) {
|
||||
let probs = [];
|
||||
|
||||
const module_validaters = [
|
||||
// require('./validation/multiple-package-decls'),
|
||||
// require('./validation/unit-decl-order'),
|
||||
// require('./validation/duplicate-members'),
|
||||
// require('./validation/parse-errors'),
|
||||
// require('./validation/modifier-errors'),
|
||||
// require('./validation/unresolved-imports'),
|
||||
// require('./validation/invalid-types'),
|
||||
// require('./validation/bad-extends'),
|
||||
// require('./validation/bad-implements'),
|
||||
// require('./validation/non-implemented-interfaces'),
|
||||
// require('./validation/bad-overrides'),
|
||||
// require('./validation/missing-constructor'),
|
||||
//require('./validation/expression-compatibility'),
|
||||
];
|
||||
let problems = [
|
||||
module_validaters.map(v => v(unit.types, unit)),
|
||||
...probs,
|
||||
];
|
||||
|
||||
function flatten(arr) {
|
||||
let res = arr;
|
||||
for (;;) {
|
||||
const idx = res.findIndex(x => Array.isArray(x));
|
||||
if (idx < 0) {
|
||||
return res;
|
||||
}
|
||||
res = [...res.slice(0, idx), ...res[idx], ...res.slice(idx+1)]
|
||||
}
|
||||
}
|
||||
|
||||
let flattened = flatten(problems).filter(x => x);
|
||||
console.log(`Problems: ${flattened.length}`)
|
||||
return flattened;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validate,
|
||||
parseMethodBodies,
|
||||
}
|
||||
55
langserver/java/validation/bad-extends.js
Normal file
55
langserver/java/validation/bad-extends.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { SourceType } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { AnyType } = require('../anys');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkExtends(source_type, probs) {
|
||||
const supertypes = source_type.extends_types
|
||||
.map(st => st.resolved)
|
||||
.filter(t => !(t instanceof AnyType));
|
||||
|
||||
if (supertypes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const supertype = supertypes[0];
|
||||
if (source_type.typeKind === 'enum') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Enum types cannot declare a superclass`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertypes.length > 1) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[1].tokens, `Class types cannot inherit from more than one type`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype.typeKind !== 'class') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype.typeKind === 'class' && supertype.modifiers.includes('final')) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from final class: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (source_type.typeKind === 'class' && supertype === source_type) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from itself`));
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
supertypes.forEach((supertype, i) => {
|
||||
if (supertype.typeKind !== 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
|
||||
}
|
||||
if (supertype === source_type) {
|
||||
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from itself`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkExtends(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
43
langserver/java/validation/bad-implements.js
Normal file
43
langserver/java/validation/bad-implements.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const {SourceType} = require('../source-types');
|
||||
const { AnyType } = require('../anys');
|
||||
const { UnresolvedType } = require('java-mti');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkImplements(source_type, probs) {
|
||||
const superinterfaces = source_type.implements_types
|
||||
.map(st => st.resolved)
|
||||
.filter(t => !(t instanceof AnyType));
|
||||
|
||||
if (superinterfaces.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.implements_types[0].tokens, `Interface types cannot declare an implements section`));
|
||||
}
|
||||
if (source_type.typeKind === 'class') {
|
||||
superinterfaces.forEach((intf, i) => {
|
||||
if (intf instanceof UnresolvedType) {
|
||||
return;
|
||||
}
|
||||
if (intf.typeKind !== 'interface') {
|
||||
probs.push(ParseProblem.Error(source_type.implements_types[i].tokens, `Class '${source_type.fullyDottedRawName}' cannot implement ${intf.typeKind} type: '${intf.fullyDottedRawName}'`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkImplements(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
65
langserver/java/validation/bad-overrides.js
Normal file
65
langserver/java/validation/bad-overrides.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const {SourceType, SourceAnnotation} = require('../source-types');
|
||||
const {CEIType, Method} = require('java-mti');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkOverrides(source_type, probs) {
|
||||
if (source_type.extends_types.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind !== 'class') {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {{ann:SourceAnnotation, method:Method, method_id:string}[]} */
|
||||
const overriden_methods = [];
|
||||
source_type.sourceMethods.reduce((arr, method) => {
|
||||
const ann = method.annotations.find(a => a.type.simpleTypeName === 'Override');
|
||||
if (ann) {
|
||||
arr.push({
|
||||
ann,
|
||||
method,
|
||||
method_id: `${method.name}${method.methodSignature}`,
|
||||
})
|
||||
}
|
||||
return arr;
|
||||
}, overriden_methods);
|
||||
|
||||
if (!overriden_methods.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methods = new Set(), supers_done = new Set();
|
||||
const supers = source_type.supers.slice();
|
||||
while (supers.length) {
|
||||
const s = supers.shift();
|
||||
supers_done.add(s);
|
||||
s.methods.forEach(m => {
|
||||
methods.add(`${m.name}${m.methodSignature}`);
|
||||
});
|
||||
if (s instanceof CEIType) {
|
||||
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
|
||||
}
|
||||
}
|
||||
|
||||
overriden_methods.forEach(x => {
|
||||
if (!methods.has(x.method_id)) {
|
||||
probs.push(ParseProblem.Error(x.ann.annotationTypeIdent.tokens, `${x.method.label} does not override a matching method in any inherited type or interface`));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkOverrides(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
53
langserver/java/validation/invalid-types.js
Normal file
53
langserver/java/validation/invalid-types.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { SourceType, SourceTypeIdent } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceTypeIdent} type
|
||||
* @param {boolean} is_return_type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkType(type, is_return_type, probs) {
|
||||
const typesig = type.resolved.typeSignature;
|
||||
if (/^\[*U/.test(typesig)) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `Unresolved type '${type.resolved.label}'`))
|
||||
return;
|
||||
}
|
||||
if (typesig === 'V' && !is_return_type) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `'void' is not a valid type for variables`))
|
||||
}
|
||||
if (/^\[+V/.test(typesig)) {
|
||||
probs.push(ParseProblem.Error(type.tokens, `Illegal type: '${type.resolved.label}'`))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkInvalidTypes(type, probs) {
|
||||
type.fields.forEach(f => checkType(f.fieldTypeIdent, false, probs));
|
||||
type.sourceMethods.forEach(m => {
|
||||
checkType(m.returnTypeIdent, true, probs);
|
||||
m.parameters.forEach(p => {
|
||||
checkType(p.paramTypeIdent, false, probs);
|
||||
})
|
||||
})
|
||||
type.constructors.forEach(c => {
|
||||
c.parameters.forEach(p => {
|
||||
checkType(p.paramTypeIdent, false, probs);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkInvalidTypes(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
40
langserver/java/validation/missing-constructor.js
Normal file
40
langserver/java/validation/missing-constructor.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const {SourceType, SourceConstructor} = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConstructor(source_type, probs) {
|
||||
if (source_type.typeKind !== 'class') {
|
||||
return;
|
||||
}
|
||||
if (source_type.constructors[0] instanceof SourceConstructor) {
|
||||
return;
|
||||
}
|
||||
const superclass = source_type.supers.find(s => s.typeKind === 'class');
|
||||
if (!superclass) {
|
||||
// if there's no superclass, the class must inherit from an interface
|
||||
// - which means the inherited class is Object (and a default constructor exists)
|
||||
return;
|
||||
}
|
||||
if (superclass.constructors.length) {
|
||||
if (!superclass.constructors.find(c => c.parameterCount === 0)) {
|
||||
// the source type has no declared constructors, but the superclass
|
||||
// does not include a default (parameterless) constructor
|
||||
probs.push(ParseProblem.Error(source_type.nameToken, `Class '${source_type.fullyDottedRawName}' requires a constructor to be declared because the inherited class '${superclass.fullyDottedRawName}' does not define a default constructor.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkConstructor(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
188
langserver/java/validation/modifier-errors.js
Normal file
188
langserver/java/validation/modifier-errors.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const { SourceType, SourceMethod, SourceParameter, SourceField, SourceConstructor, SourceInitialiser } = require('../source-types');
|
||||
const { Token } = require('../tokenizer');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkDuplicate(mods, probs) {
|
||||
if (mods.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const m = new Map();
|
||||
for (let mod of mods) {
|
||||
const firstmod = m.get(mod.source);
|
||||
if (firstmod === undefined) {
|
||||
m.set(mod.source, mod);
|
||||
} else {
|
||||
probs.push(ParseProblem.Error(mod, 'Duplicate modifier'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} mods
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConflictingAccess(mods, probs) {
|
||||
if (mods.length <= 1) {
|
||||
return;
|
||||
}
|
||||
const allmods = mods.map(m => m.source).join(' ');
|
||||
for (let mod of mods) {
|
||||
let match;
|
||||
switch (mod.source) {
|
||||
case 'private':
|
||||
match = allmods.match(/protected|public/);
|
||||
break;
|
||||
case 'protected':
|
||||
match = allmods.match(/private|public/);
|
||||
break;
|
||||
case 'public':
|
||||
match = allmods.match(/private|protected/);
|
||||
break;
|
||||
}
|
||||
if (match) {
|
||||
probs.push(ParseProblem.Error(mod, `Access modifier '${mod.source}' conflicts with '${match[0]}'`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceField} field
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkFieldModifiers(field, probs) {
|
||||
checkDuplicate(field.modifierTokens, probs);
|
||||
checkConflictingAccess(field.modifierTokens, probs);
|
||||
for (let mod of field.modifierTokens) {
|
||||
switch (mod.source) {
|
||||
case 'abstract':
|
||||
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be abstract'));
|
||||
break;
|
||||
case 'native':
|
||||
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be native'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceParameter} param
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkParameterModifiers(param, probs) {
|
||||
// the only permitted modifier is final
|
||||
let has_final = false;
|
||||
param.modifierTokens.forEach(mod => {
|
||||
if (mod.value === 'final') {
|
||||
if (has_final) {
|
||||
probs.push(ParseProblem.Error(mod, `Repeated modifier: final`));
|
||||
}
|
||||
has_final = true;
|
||||
return;
|
||||
}
|
||||
probs.push(ParseProblem.Error(mod, `Parameter declarations cannot be ${mod.value}`));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {Map<string,*>} ownertypemods
|
||||
* @param {SourceMethod} method
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkMethodModifiers(type, ownertypemods, method, probs) {
|
||||
checkDuplicate(method.modifierTokens, probs);
|
||||
checkConflictingAccess(method.modifierTokens, probs);
|
||||
|
||||
method.parameters.forEach(p => checkParameterModifiers(p, probs));
|
||||
|
||||
const allmods = new Map(method.modifierTokens.map(m => [m.source, m]));
|
||||
const is_interface_kind = /@?interface/.test(type.typeKind);
|
||||
const has_body = method.hasImplementation;
|
||||
|
||||
if (allmods.has('abstract') && allmods.has('final')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and final'));
|
||||
}
|
||||
if (allmods.has('abstract') && allmods.has('native')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and native'));
|
||||
}
|
||||
if (allmods.has('abstract') && has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations marked as abstract cannot have a method body'));
|
||||
}
|
||||
if (!is_interface_kind && !allmods.has('abstract') && !allmods.has('native') && !has_body) {
|
||||
probs.push(ParseProblem.Error(method.nameToken, `Method '${method.name}' must have an implementation or be defined as abstract or native`));
|
||||
}
|
||||
if (!is_interface_kind && allmods.has('abstract') && !ownertypemods.has('abstract')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('abstract'), `Method '${method.name}' cannot be declared abstract inside a non-abstract type`));
|
||||
}
|
||||
if (is_interface_kind && has_body && !allmods.has('default')) {
|
||||
probs.push(ParseProblem.Error(method.body[0], `Non-default interface methods cannot have a method body`));
|
||||
}
|
||||
if (allmods.has('native') && has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('native'), 'Method declarations marked as native cannot have a method body'));
|
||||
}
|
||||
// JLS8
|
||||
if (type.typeKind !== 'interface' && allmods.has('default')) {
|
||||
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations are only allowed inside interfaces`));
|
||||
}
|
||||
if (allmods.has('default') && !has_body) {
|
||||
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations must have an implementation`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceConstructor} field
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkConstructorModifiers(field, probs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceInitialiser} initialiser
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkInitialiserModifiers(initialiser, probs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} type
|
||||
* @param {ParseProblem[]} probs
|
||||
*/
|
||||
function checkTypeModifiers(type, probs) {
|
||||
const typemods = new Map(type.modifierTokens.map(m => [m.source, m]));
|
||||
checkDuplicate(type.modifierTokens, probs);
|
||||
|
||||
if (type.typeKind === 'interface' && typemods.has('final')) {
|
||||
probs.push(ParseProblem.Error(typemods.get('final'), 'Interface declarations cannot be marked as final'));
|
||||
}
|
||||
if (type.typeKind === 'enum' && typemods.has('abstract')) {
|
||||
probs.push(ParseProblem.Error(typemods.get('abstract'), 'Enum declarations cannot be marked as abstract'));
|
||||
}
|
||||
if (/[$]/.test(type._rawShortSignature)) {
|
||||
checkConflictingAccess(type.modifierTokens, probs);
|
||||
} else {
|
||||
// top-level types cannot be private, protected or static
|
||||
for (let mod of ['private','protected', 'static']) {
|
||||
if (typemods.has(mod)) {
|
||||
probs.push(ParseProblem.Error(typemods.get(mod), `Top-level declarations cannot be marked as ${mod}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type.fields.forEach(field => checkFieldModifiers(field, probs));
|
||||
type.sourceMethods.forEach(method => checkMethodModifiers(type, typemods, method, probs));
|
||||
type.constructors.forEach(ctr => checkConstructorModifiers(ctr, probs));
|
||||
type.initers.forEach(initer => checkInitialiserModifiers(initer, probs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} types
|
||||
*/
|
||||
module.exports = function(types) {
|
||||
const probs = [];
|
||||
types.forEach(type => checkTypeModifiers(type, probs));
|
||||
return probs;
|
||||
}
|
||||
85
langserver/java/validation/non-implemented-interfaces.js
Normal file
85
langserver/java/validation/non-implemented-interfaces.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
const { SourceType } = require('../source-types');
|
||||
const { CEIType, Method} = require('java-mti');
|
||||
const {isTypeAssignable} = require('../expression-resolver');
|
||||
|
||||
function nonAbstractLabel(label) {
|
||||
return label.replace(/\babstract /g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Method} impl method implementation
|
||||
* @param {Method} method interface method
|
||||
*/
|
||||
function isMethodCompatible(impl, method) {
|
||||
const impl_params = impl.parameters;
|
||||
const method_params = method.parameters;
|
||||
if (impl_params.length !== method_params.length) {
|
||||
return false;
|
||||
}
|
||||
return impl_params.every((p,idx) => isTypeAssignable(method_params[idx].type, p.type))
|
||||
&& isTypeAssignable(method.returnType, impl.returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType} source_type
|
||||
* @param {*} probs
|
||||
*/
|
||||
function checkImplementedInterfaces(source_type, probs) {
|
||||
if (source_type.implements_types.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (source_type.typeKind === 'interface') {
|
||||
return;
|
||||
}
|
||||
if (source_type.modifiers.includes('abstract')) {
|
||||
return;
|
||||
}
|
||||
/** @type {Set<CEIType>} */
|
||||
const interfaces = new Set(), supers_done = new Set();
|
||||
const supers = source_type.supers.slice();
|
||||
while (supers.length) {
|
||||
const s = supers.shift();
|
||||
supers_done.add(s);
|
||||
if (s instanceof CEIType) {
|
||||
if (s.typeKind === 'interface') {
|
||||
interfaces.add(s);
|
||||
}
|
||||
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
|
||||
}
|
||||
}
|
||||
|
||||
const implemented = source_type.methods.map(m => `${m.name}${m.methodSignature}`);
|
||||
interfaces.forEach((intf, i) => {
|
||||
const missing_methods = [];
|
||||
intf.methods.forEach(m => {
|
||||
// default methods don't require implementing
|
||||
if (m.hasImplementation) {
|
||||
return;
|
||||
}
|
||||
const namedsig = `${m.name}${m.methodSignature}`
|
||||
if (implemented.indexOf(namedsig) < 0) {
|
||||
// perform a more detailed search for a compatible match
|
||||
if (!source_type.methods.find(source_method => source_method.name === m.name && isMethodCompatible(source_method, m))) {
|
||||
missing_methods.push(nonAbstractLabel(m.label));
|
||||
}
|
||||
}
|
||||
})
|
||||
if (missing_methods.length) {
|
||||
probs.push(ParseProblem.Error(source_type.kind_token, `Non-abstract ${source_type.typeKind} '${source_type.fullyDottedRawName}' does not implement the following methods from interface '${intf.fullyDottedTypeName}':\n${missing_methods.join('\n')}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceType[]} source_types
|
||||
*/
|
||||
module.exports = function(source_types) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
source_types.forEach(type => checkImplementedInterfaces(type, probs));
|
||||
|
||||
return probs;
|
||||
}
|
||||
17
langserver/java/validation/unresolved-imports.js
Normal file
17
langserver/java/validation/unresolved-imports.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { SourceUnit } = require('../source-types');
|
||||
const ParseProblem = require('../parsetypes/parse-problem');
|
||||
|
||||
/**
|
||||
* @param {SourceUnit} unit
|
||||
*/
|
||||
module.exports = function(mod, unit) {
|
||||
/** @type {ParseProblem[]} */
|
||||
const probs = [];
|
||||
|
||||
unit.imports.forEach(i => {
|
||||
if (!i.resolved)
|
||||
probs.push(ParseProblem.Warning(i.nameTokens, `Unresolved import: ${i.package_name}`));
|
||||
})
|
||||
|
||||
return probs;
|
||||
}
|
||||
60
langserver/logging.js
Normal file
60
langserver/logging.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { Settings } = require('./settings');
|
||||
|
||||
const earlyTraceBuffer = [];
|
||||
|
||||
/**
|
||||
* Log a trace message with a timestamp - only logs if "Trace enabled" in settings
|
||||
* @param {string} s
|
||||
*/
|
||||
function trace(s) {
|
||||
if (Settings.updateCount > 0 && !Settings.trace) {
|
||||
return;
|
||||
}
|
||||
const msg = `${Date.now()}: ${s}`;
|
||||
// before we've retrieved the trace setting, buffer the messages
|
||||
if (Settings.updateCount === 0) {
|
||||
earlyTraceBuffer.push(msg);
|
||||
return;
|
||||
}
|
||||
if (earlyTraceBuffer.length) {
|
||||
earlyTraceBuffer.splice(0, earlyTraceBuffer.length).forEach(msg => console.log(msg));
|
||||
}
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function info(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of active timers
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const timersRunning = new Set();
|
||||
|
||||
/**
|
||||
* Starts a named timer using `console.time()` - only if "Trace Enabled" in Settings
|
||||
* @param {string} label
|
||||
*/
|
||||
function time(label) {
|
||||
if (Settings.trace) {
|
||||
timersRunning.add(label);
|
||||
console.time(label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a named timer (and prints the elapsed time) using `console.timeEnd()`
|
||||
* @param {string} label
|
||||
*/
|
||||
function timeEnd(label) {
|
||||
if (timersRunning.has(label)) {
|
||||
timersRunning.delete(label);
|
||||
console.timeEnd(label);
|
||||
}
|
||||
}
|
||||
|
||||
exports.info = info;
|
||||
exports.trace = trace;
|
||||
exports.time = time;
|
||||
exports.timeEnd = timeEnd;
|
||||
92
langserver/method-signatures.js
Normal file
92
langserver/method-signatures.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const { Method } = require('java-mti');
|
||||
const { indexAt } = require('./document');
|
||||
const { formatDoc } = require('./doc-formatter');
|
||||
const { trace } = require('./logging');
|
||||
const { event } = require('./analytics');
|
||||
|
||||
let methodsigRequestCount = 0;
|
||||
|
||||
/**
|
||||
* Retrieve method signature information
|
||||
*
|
||||
* Each parsed token that is relevant to a method call is
|
||||
* tagged with the list of possible methods and the best matched
|
||||
* method. The tagged tokens include:
|
||||
* - the opening bracket
|
||||
* - each token in every argument
|
||||
* - each comma between the arguments
|
||||
*
|
||||
* The function locates the nearest non-ws token and checks
|
||||
* for any tagged method-call info. It then converts it
|
||||
* to the relevant vscode method signature structure for display.
|
||||
*
|
||||
* @param {import('vscode-languageserver').SignatureHelpParams} request
|
||||
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
|
||||
*/
|
||||
async function getSignatureHelp(request, liveParsers) {
|
||||
trace('getSignatureHelp');
|
||||
/** @type {import('vscode-languageserver').SignatureHelp} */
|
||||
let sighelp = {
|
||||
signatures: [],
|
||||
activeSignature: 0,
|
||||
activeParameter: 0,
|
||||
}
|
||||
const docinfo = liveParsers.get(request.textDocument.uri);
|
||||
if (!docinfo || !docinfo.parsed) {
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
// wait for any active edits to complete
|
||||
await docinfo.reparseWaiter;
|
||||
|
||||
methodsigRequestCount += 1;
|
||||
if ((methodsigRequestCount === 1) || (methodsigRequestCount === 5) || ((methodsigRequestCount % 25) === 0)) {
|
||||
event('method-sig-requests', { methsig_req_count: methodsigRequestCount });
|
||||
}
|
||||
|
||||
// locate the token at the requested position
|
||||
const index = indexAt(request.position, docinfo.content);
|
||||
const token = docinfo.parsed.unit.getTokenAt(index);
|
||||
if (!token || !token.methodCallInfo) {
|
||||
trace('onSignatureHelp - no method call info');
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
// the token has method information attached to it
|
||||
// - convert it to the required vscode format
|
||||
trace(`onSignatureHelp - ${token.methodCallInfo.methods.length} methods`);
|
||||
sighelp = {
|
||||
signatures: token.methodCallInfo.methods.map(m => {
|
||||
const documentation = formatDoc(`#### ${m.owner.simpleTypeName}${m instanceof Method ? `.${m.name}` : ''}()`, m.docs);
|
||||
const param_docs = new Map();
|
||||
if (documentation) {
|
||||
// extract each of the @param sections (if any)
|
||||
for (let m, re=/@param\s+(\S+)([\d\D]+?)(?=\n\n|\n[ \t*]*@\w+|$)/g; m = re.exec(documentation.value);) {
|
||||
param_docs.set(m[1], m[2]);
|
||||
}
|
||||
}
|
||||
/** @type {import('vscode-languageserver').SignatureInformation} */
|
||||
let si = {
|
||||
label: m.label,
|
||||
documentation,
|
||||
parameters: m.parameters.map(p => {
|
||||
/** @type {import('vscode-languageserver').ParameterInformation} */
|
||||
let pi = {
|
||||
documentation: {
|
||||
kind: 'markdown',
|
||||
value: param_docs.has(p.name) ? `**${p.name}**: ${param_docs.get(p.name)}` : '',
|
||||
},
|
||||
label: p.label
|
||||
}
|
||||
return pi;
|
||||
})
|
||||
}
|
||||
return si;
|
||||
}),
|
||||
activeSignature: token.methodCallInfo.methodIdx,
|
||||
activeParameter: token.methodCallInfo.argIdx,
|
||||
}
|
||||
return sighelp;
|
||||
}
|
||||
|
||||
exports.getSignatureHelp = getSignatureHelp;
|
||||
352
langserver/package-lock.json
generated
Normal file
352
langserver/package-lock.json
generated
Normal file
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "13.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
|
||||
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
|
||||
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.48",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
||||
},
|
||||
"binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
|
||||
"requires": {
|
||||
"buffers": "~0.1.1",
|
||||
"chainsaw": "~0.1.0"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-indexof-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8="
|
||||
},
|
||||
"buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
|
||||
},
|
||||
"chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
|
||||
"requires": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
|
||||
"requires": {
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fstream": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
|
||||
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"inherits": "~2.0.0",
|
||||
"mkdirp": ">=0.5 0",
|
||||
"rimraf": "2"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
|
||||
"requires": {
|
||||
"agent-base": "^4.3.0",
|
||||
"debug": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"java-mti": {
|
||||
"version": "github:adelphes/java-mti#d0e1e45bad4d2bba453dbcb5ad527db023f223e8",
|
||||
"from": "github:adelphes/java-mti#d0e1e45",
|
||||
"requires": {
|
||||
"unzipper": "^0.10.11"
|
||||
}
|
||||
},
|
||||
"listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"mixpanel": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.11.0.tgz",
|
||||
"integrity": "sha512-TS7AkCmfC+vGshlCOjEcITFoFxlt5fdSEqmN+d+pTXAhE5v+jPQW2uUcn9W+Oq4NVXz+kdskU09dsm9vmNl0ig==",
|
||||
"requires": {
|
||||
"https-proxy-agent": "3.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
|
||||
},
|
||||
"unzipper": {
|
||||
"version": "0.10.11",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
|
||||
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
|
||||
"requires": {
|
||||
"big-integer": "^1.6.17",
|
||||
"binary": "~0.3.0",
|
||||
"bluebird": "~3.4.1",
|
||||
"buffer-indexof-polyfill": "~1.0.0",
|
||||
"duplexer2": "~0.1.4",
|
||||
"fstream": "^1.0.12",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"listenercount": "~1.0.1",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "~1.0.4"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
|
||||
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
|
||||
},
|
||||
"vscode-languageserver": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
|
||||
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
|
||||
"requires": {
|
||||
"vscode-languageserver-protocol": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-protocol": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
|
||||
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
|
||||
"requires": {
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageserver-types": "3.15.1"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-textdocument": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
|
||||
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
|
||||
},
|
||||
"vscode-languageserver-types": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
|
||||
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
|
||||
},
|
||||
"vscode-uri": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz",
|
||||
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A=="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
||||
22
langserver/package.json
Normal file
22
langserver/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.3",
|
||||
"description": "Language server for Android development",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"java-mti": "adelphes/java-mti#d0e1e45",
|
||||
"mixpanel": "0.11.0",
|
||||
"uuid": "8.2.0",
|
||||
"vscode-languageserver": "6.1.1",
|
||||
"vscode-languageserver-textdocument": "1.0.1",
|
||||
"vscode-uri": "2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.13.4"
|
||||
}
|
||||
}
|
||||
300
langserver/server.js
Normal file
300
langserver/server.js
Normal file
@@ -0,0 +1,300 @@
|
||||
const {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
ProposedFeatures,
|
||||
DidChangeConfigurationNotification,
|
||||
TextDocumentSyncKind,
|
||||
} = require('vscode-languageserver');
|
||||
const fs = require('fs');
|
||||
|
||||
const { TextDocument } = require('vscode-languageserver-textdocument');
|
||||
const { URI } = require('vscode-uri');
|
||||
|
||||
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
|
||||
const { CEIType } = require('java-mti');
|
||||
|
||||
const { Settings } = require('./settings');
|
||||
const { trace } = require('./logging');
|
||||
const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem } = require('./completions');
|
||||
const { getSignatureHelp } = require('./method-signatures');
|
||||
const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document');
|
||||
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const analytics = require('./analytics');
|
||||
const package_json = require('./package.json');
|
||||
|
||||
/**
|
||||
* The global map of Android system types
|
||||
* @typedef {Map<string, CEIType>} AndroidLibrary
|
||||
* @type {AndroidLibrary|Promise<AndroidLibrary>}
|
||||
*/
|
||||
let androidLibrary = null;
|
||||
|
||||
/**
|
||||
* The list of loaded Java documents
|
||||
* @type {Map<string,JavaDocInfo>}
|
||||
*/
|
||||
const liveParsers = new FileURIMap();
|
||||
|
||||
let startupOpts = null;
|
||||
let hasConfigurationCapability = false;
|
||||
let hasWorkspaceFolderCapability = false;
|
||||
|
||||
function loadCodeCompletionLibrary(extensionPath, codeCompletionLibraries) {
|
||||
// the android library is loaded asynchronously, with the global `androidLibrary` variable
|
||||
// set to the promise while it is loading.
|
||||
androidLibrary = (androidLibrary instanceof Promise
|
||||
? androidLibrary // if we're currently loading, wait for it to complete
|
||||
: Promise.resolve(new Map())
|
||||
)
|
||||
.then(() => loadAndroidSystemLibrary(extensionPath, codeCompletionLibraries))
|
||||
.then(
|
||||
library => androidLibrary = library,
|
||||
err => {
|
||||
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
|
||||
return new Map();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Text document manager monitoring file opens and edits
|
||||
let documents = new TextDocuments({
|
||||
/**
|
||||
*
|
||||
* @param {string} uri
|
||||
* @param {string} languageId
|
||||
* @param {number} version
|
||||
* @param {string} content
|
||||
*/
|
||||
create(uri, languageId, version, content) {
|
||||
trace(`document create ${uri}:${version}`);
|
||||
|
||||
// sanity-check - we only support Java source files
|
||||
if (!/\.java$/i.test(uri)) {
|
||||
return { uri };
|
||||
}
|
||||
|
||||
// add the document to the set
|
||||
liveParsers.set(uri, new JavaDocInfo(uri, content, version));
|
||||
|
||||
// tokenize the file content and build the initial parse state
|
||||
reparse([uri], liveParsers, androidLibrary, { includeMethods: true });
|
||||
|
||||
return { uri };
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {TextDocument} document
|
||||
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
|
||||
* @param {number} version
|
||||
*/
|
||||
update(document, changes, version) {
|
||||
trace(`document update ${document.uri}:${version}`);
|
||||
if (!liveParsers.has(document.uri)) {
|
||||
return;
|
||||
}
|
||||
const docinfo = liveParsers.get(document.uri);
|
||||
if (!docinfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply the edits to our local content copy
|
||||
changes.forEach((change) => {
|
||||
/** @type {import('vscode-languageserver').Range} */
|
||||
const r = change['range'];
|
||||
if (r) {
|
||||
const start_index = indexAt(r.start, docinfo.content);
|
||||
let end_index = start_index + (r.end.character - r.start.character);
|
||||
if (r.end.line !== r.start.line) end_index = indexAt(r.end, docinfo.content);
|
||||
docinfo.content = `${docinfo.content.slice(0, start_index)}${change.text}${docinfo.content.slice(end_index)}`;
|
||||
}
|
||||
});
|
||||
|
||||
docinfo.version = version;
|
||||
docinfo.scheduleReparse(liveParsers, androidLibrary);
|
||||
|
||||
return document;
|
||||
},
|
||||
});
|
||||
|
||||
// Create a connection for the server. The connection uses Node's IPC as a transport.
|
||||
const connection = createConnection(ProposedFeatures.all);
|
||||
|
||||
connection.onInitialize((params) => {
|
||||
|
||||
startupOpts = {
|
||||
extensionPath: '',
|
||||
initialSettings: {
|
||||
appSourceRoot: '',
|
||||
/** @type {string[]} */
|
||||
codeCompletionLibraries: [],
|
||||
trace: false,
|
||||
},
|
||||
sourceFiles: [],
|
||||
...params.initializationOptions,
|
||||
}
|
||||
|
||||
Settings.set(startupOpts.initialSettings);
|
||||
analytics.init(undefined, startupOpts.mpuid, uuidv4(), package_json, { vscode_version: startupOpts.vscodeVersion });
|
||||
|
||||
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
|
||||
|
||||
let capabilities = params.capabilities;
|
||||
|
||||
// Does the client support the `workspace/configuration` request?
|
||||
// If not, we will fall back using global settings
|
||||
hasConfigurationCapability = capabilities.workspace && !!capabilities.workspace.configuration;
|
||||
|
||||
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
|
||||
|
||||
/** @type {string[]} */
|
||||
const file_uris = Array.isArray(startupOpts.sourceFiles) ? startupOpts.sourceFiles : [];
|
||||
for (let file_uri of file_uris) {
|
||||
const file = URI.parse(file_uri, true);
|
||||
const filePath = file.fsPath;
|
||||
if (!/.java/i.test(filePath)) {
|
||||
trace(`ignoring non-java file: ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
if (liveParsers.has(file_uri)) {
|
||||
trace(`File already loaded: ${file_uri}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// it's fine to load the initial file set synchronously - the language server runs in a
|
||||
// separate process and nothing (useful) can happen until the first parse is complete.
|
||||
const content = fs.readFileSync(file.fsPath, 'utf8');
|
||||
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
|
||||
trace(`Added initial file: ${file_uri}`);
|
||||
} catch (err) {
|
||||
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
|
||||
}
|
||||
}
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
|
||||
|
||||
return {
|
||||
capabilities: {
|
||||
// we support incremental updates
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
|
||||
// we support code completion
|
||||
completionProvider: {
|
||||
resolveProvider: true,
|
||||
},
|
||||
|
||||
// we support method signature information
|
||||
signatureHelpProvider : {
|
||||
triggerCharacters: [ '(' ]
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
connection.onInitialized(async () => {
|
||||
if (hasConfigurationCapability) {
|
||||
// Register for all configuration changes.
|
||||
connection.client.register(
|
||||
DidChangeConfigurationNotification.type, {
|
||||
section: 'android-dev-ext',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
|
||||
trace('Workspace folder change event received.');
|
||||
});
|
||||
}
|
||||
|
||||
trace('Initialization complete');
|
||||
});
|
||||
|
||||
connection.onDidChangeConfiguration(async (change) => {
|
||||
trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`);
|
||||
|
||||
const prev_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
|
||||
|
||||
// fetch and update the settings
|
||||
const newSettings = await connection.workspace.getConfiguration({
|
||||
section: "android-dev-ext"
|
||||
});
|
||||
|
||||
Settings.set(newSettings);
|
||||
|
||||
if (Settings.updateCount > 2) {
|
||||
analytics.event('ls-settings-changed', {
|
||||
appSourceRoot: Settings.appSourceRoot,
|
||||
libs: Settings.codeCompletionLibraries,
|
||||
trace: Settings.trace,
|
||||
})
|
||||
}
|
||||
|
||||
const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
|
||||
if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) {
|
||||
// code-completion libraries have changed - reload the android library
|
||||
trace("code completion libraries changed - reloading android library and reparsing")
|
||||
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false });
|
||||
clearDefaultCompletionEntries();
|
||||
}
|
||||
})
|
||||
|
||||
documents.onDidClose((e) => {
|
||||
trace(`doc closed ${e.document.uri}`);
|
||||
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
|
||||
});
|
||||
|
||||
connection.onDidChangeWatchedFiles(
|
||||
/** @param {import('vscode-languageserver').DidChangeWatchedFilesParams} params */
|
||||
(params) => {
|
||||
// Monitored files have change in VS Code
|
||||
trace(`watch file change: ${JSON.stringify(params)}`);
|
||||
let files_changed = false;
|
||||
params.changes.forEach(change => {
|
||||
switch(change.type) {
|
||||
case 1: // create
|
||||
// if the user creates the file directly in vscode, the file will automatically open (and we receive an open callback)
|
||||
// - but if the user creates or copies a file into the workspace, we need to manually add it to the set.
|
||||
if (!liveParsers.has(change.uri)) {
|
||||
trace(`file added: ${change.uri}`)
|
||||
try {
|
||||
const fname = URI.parse(change.uri, true).fsPath;
|
||||
liveParsers.set(change.uri, new JavaDocInfo(change.uri, fs.readFileSync(fname, 'utf8'), 0));
|
||||
files_changed = true;
|
||||
} catch (err) {
|
||||
console.log(`Failed to add new file '${change.uri}' to working set. ${err.message}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // change
|
||||
// called when the user manually saves the file - ignore for now
|
||||
break;
|
||||
case 3: // delete
|
||||
trace(`file deleted: ${change.uri}`)
|
||||
liveParsers.delete(change.uri);
|
||||
files_changed = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (files_changed) {
|
||||
// reparse the entire set
|
||||
reparse([...liveParsers.keys()], liveParsers, androidLibrary);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Retrieve the initial list of the completion items.
|
||||
connection.onCompletion(params => getCompletionItems(params, liveParsers, androidLibrary));
|
||||
|
||||
// Resolve additional information for the item selected in the completion list.
|
||||
connection.onCompletionResolve(item => resolveCompletionItem(item));
|
||||
|
||||
// Retrieve method signature information
|
||||
connection.onSignatureHelp(params => getSignatureHelp(params, liveParsers));
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
42
langserver/settings.js
Normal file
42
langserver/settings.js
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
const defaultSettings = {
|
||||
appSourceRoot: 'app/src/main',
|
||||
codeCompletionLibraries: [],
|
||||
trace: false,
|
||||
}
|
||||
|
||||
class AndroidProjectSettings {
|
||||
/**
|
||||
* The root of the app source folder.
|
||||
* This folder should contain AndroidManifest.xml as well as the asets, res, etc folders
|
||||
*/
|
||||
appSourceRoot = defaultSettings.appSourceRoot;
|
||||
|
||||
/**
|
||||
* The set of androidx libraries to include in code completion
|
||||
*/
|
||||
codeCompletionLibraries = defaultSettings.codeCompletionLibraries;
|
||||
|
||||
/**
|
||||
* True if we log details
|
||||
*/
|
||||
trace = defaultSettings.trace;
|
||||
|
||||
updateCount = 0;
|
||||
|
||||
static Instance = new AndroidProjectSettings();
|
||||
|
||||
set(values) {
|
||||
if (!values || typeof values !== 'object') {
|
||||
return;
|
||||
}
|
||||
this.updateCount += 1;
|
||||
for (let key in defaultSettings) {
|
||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
||||
this[key] = values[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Settings = AndroidProjectSettings.Instance;
|
||||
98
langserver/tests/test-tokenizer.js
Normal file
98
langserver/tests/test-tokenizer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { tokenize } = require('../java/tokenizer');
|
||||
|
||||
function testTokenize() {
|
||||
const tests = [
|
||||
// the basics
|
||||
{ src: 'i', r: [{value: 'i', kind:'ident'}] },
|
||||
{ src: '0', r: [{value: '0', kind:'int-number-literal'}] },
|
||||
{ src: `""`, r: [{value: `""`, kind:'string-literal'}] },
|
||||
{ src: `'x'`, r: [{value: `'x'`, kind:'char-literal'}] },
|
||||
{ src: `(`, r: [{value: `(`, kind:'open-bracket'}] },
|
||||
...'. , [ ] ? : @'.split(' ').map(symbol => ({ src: symbol, r: [{value: symbol, kind: 'symbol'}] })),
|
||||
...'= += -= *= /= %= >>= <<= &= |= ^='.split(' ').map(op => ({ src: op, r: [{value: op, kind:'assignment-operator'}] })),
|
||||
...'+ -'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'plumin-operator'}] })),
|
||||
...'* / %'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'muldiv-operator'}] })),
|
||||
...'# ¬'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'invalid'}] })),
|
||||
|
||||
// numbers - decimal with exponent
|
||||
...'0.0e+0 0.0E+0 0e+0 0e0 .0e0 0e0f 0e0d'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
|
||||
// numbers - decimal with partial exponent
|
||||
...'0.0e+ 0.0E+ 0e+ 0e .0e 0ef 0ed'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
|
||||
// numbers - not decimal exponent
|
||||
{ src: '0.0ea', r: [{value: '0.0e', kind:'dec-exp-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
|
||||
// numbers - decimal (no exponent)
|
||||
...'0.123 0. 0.f 0.0D .0 .0f .123D'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-number-literal'}] })),
|
||||
// numbers - not decimal
|
||||
{ src: '0.a', r: [{value: '0.', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
{ src: '0.0a', r: [{value: '0.0', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
|
||||
|
||||
// numbers - hex
|
||||
...'0x0 0x123456789abcdef 0xABCDEF 0xabcdefl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
|
||||
// numbers - partial hex
|
||||
...'0x 0xl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
|
||||
|
||||
// numbers - decimal
|
||||
...'0 123456789 0l'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'int-number-literal'}] })),
|
||||
|
||||
// strings
|
||||
...[`"abc"`, `"\\n"`, `"\\""`].map(num => ({ src: num, r: [{value: num, kind:'string-literal'}] })),
|
||||
// unterminated strings
|
||||
...[`"abc`, `"\\n`, `"\\"`, `"`].map(num => ({ src: num, r: [{value: num, kind:'unterminated-string-literal'}] })),
|
||||
// strings cannot cross newlines
|
||||
{ src: `"abc\n`, r: [{value: `"abc`, kind:'unterminated-string-literal'}, {value: '\n', kind:'wsc'}] },
|
||||
|
||||
// characters
|
||||
...[`'a'`, `'\\n'`, `'\\''`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
|
||||
// unterminated/invalid characters
|
||||
...[`'a`, `'\\n`, `'\\'`, `''`, `'`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
|
||||
// characters cannot cross newlines
|
||||
{ src: `'\n`, r: [{value: `'`, kind:'char-literal'}, {value: '\n', kind:'wsc'}] },
|
||||
|
||||
// arity symbol
|
||||
{ src: `int...x`, r: [
|
||||
{value: `int`, kind:'primitive-type'},
|
||||
{value: `...`, kind:'symbol'},
|
||||
{value: `x`, kind:'ident'},
|
||||
],},
|
||||
|
||||
// complex inc - the javac compiler doesn't bother to try and sensibly separate +++ - it just appears to
|
||||
// prioritise ++ in every case, assuming that the developer will insert spaces as required.
|
||||
// e.g this first one fails to compile with javac
|
||||
{ src: '++abc+++def', r: [
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'abc', kind:'ident'},
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: '+', kind:'plumin-operator'},
|
||||
{value: 'def', kind:'ident'},
|
||||
] },
|
||||
// this should be ok
|
||||
{ src: '++abc+ ++def', r: [
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'abc', kind:'ident'},
|
||||
{value: '+', kind:'plumin-operator'},
|
||||
{value: ' ', kind:'wsc'},
|
||||
{value: '++', kind:'inc-operator'},
|
||||
{value: 'def', kind:'ident'},
|
||||
] },
|
||||
]
|
||||
const report = (test, msg) => {
|
||||
console.log(JSON.stringify({test, msg}));
|
||||
}
|
||||
tests.forEach(t => {
|
||||
const tokens = tokenize(t.src);
|
||||
if (tokens.length !== t.r.length) {
|
||||
report(t, `Wrong token count. Expected ${t.r.length}, got ${tokens.length}`);
|
||||
return;
|
||||
}
|
||||
for (let i=0; i < tokens.length; i++) {
|
||||
if (tokens[i].value !== t.r[i].value)
|
||||
report(t, `Wrong token value. Expected ${t.r[i].value}, got ${tokens[i].value}`);
|
||||
if (tokens[i].kind !== t.r[i].kind)
|
||||
report(t, `Wrong token kind. Expected ${t.r[i].kind}, got ${tokens[i].kind}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
testTokenize();
|
||||
55
package-lock.json
generated
55
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "android-dev-ext",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -43,9 +43,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/vscode": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.24.0.tgz",
|
||||
"integrity": "sha512-Zu4LCYJXkMcae01zGf8IFcMeDHZo509MgK/OW7qo/Zw/ZoeYSHpchrsSVPHrXxNeFC5CPAZ210c7tM7EnImz+w==",
|
||||
"version": "1.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.46.0.tgz",
|
||||
"integrity": "sha512-8m9wPEB2mcRqTWNKs9A9Eqs8DrQZt0qNFO8GkxBOnyW6xR//3s77SoMgb/nY1ctzACsZXwZj3YRTDsn4bAoaUw==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
@@ -1214,9 +1214,9 @@
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
||||
},
|
||||
"vscode-debugadapter": {
|
||||
"version": "1.40.0",
|
||||
@@ -1232,6 +1232,41 @@
|
||||
"resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0.tgz",
|
||||
"integrity": "sha512-Fwze+9qbLDPuQUhtITJSu/Vk6zIuakNM1iR2ZiZRgRaMEgBpMs2JSKaT0chrhJHCOy6/UbpsUbUBIseF6msV+g=="
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
|
||||
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
|
||||
},
|
||||
"vscode-languageclient": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz",
|
||||
"integrity": "sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==",
|
||||
"requires": {
|
||||
"semver": "^6.3.0",
|
||||
"vscode-languageserver-protocol": "^3.15.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-protocol": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
|
||||
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
|
||||
"requires": {
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageserver-types": "3.15.1"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-types": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
|
||||
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
@@ -1262,9 +1297,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
|
||||
"integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
|
||||
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.1.31",
|
||||
|
||||
210
package.json
210
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "android-dev-ext",
|
||||
"displayName": "Android",
|
||||
"description": "Android debugging support for VS Code",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"publisher": "adelphes",
|
||||
"preview": true,
|
||||
"license": "MIT",
|
||||
@@ -20,7 +20,8 @@
|
||||
"activationEvents": [
|
||||
"onCommand:android-dev-ext.view_logcat",
|
||||
"onCommand:PickAndroidDevice",
|
||||
"onCommand:PickAndroidProcess"
|
||||
"onCommand:PickAndroidProcess",
|
||||
"onLanguage:java"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,6 +29,191 @@
|
||||
},
|
||||
"main": "./extension",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Android",
|
||||
"properties": {
|
||||
"android-dev-ext.languageSupport": {
|
||||
"scope": "resource",
|
||||
"type": "boolean",
|
||||
"default": "true",
|
||||
"description": "Enable Java language support for Android"
|
||||
},
|
||||
"android-dev-ext.appSourceRoot": {
|
||||
"scope": "resource",
|
||||
"type": "string",
|
||||
"default": "app/src/main",
|
||||
"description": "Workspace-relative path to the app source files. The specified folder should contain AndroidManifest.xml.\r\nChanges to this field require the extension or workspace to be reloaded."
|
||||
},
|
||||
"android-dev-ext.codeCompletionLibraries": {
|
||||
"scope": "resource",
|
||||
"type": "array",
|
||||
"description": "Select which Android Jetpack Libraries (androidx.*) to include in code-completion results.\nNote: Switch to the JSON Settings editor for simpler editing of this list.",
|
||||
"examples": [
|
||||
[
|
||||
"androidx.activity:activity"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"androidx.activity:activity",
|
||||
"androidx.annotation:annotation",
|
||||
"androidx.annotation:annotation-experimental",
|
||||
"androidx.annotation:annotation-experimental-lint",
|
||||
"androidx.appcompat:appcompat",
|
||||
"androidx.appcompat:appcompat-resources",
|
||||
"androidx.arch.core:core-common",
|
||||
"androidx.arch.core:core-runtime",
|
||||
"androidx.arch.core:core-testing",
|
||||
"androidx.asynclayoutinflater:asynclayoutinflater",
|
||||
"androidx.autofill:autofill",
|
||||
"androidx.benchmark:benchmark-common",
|
||||
"androidx.benchmark:benchmark-gradle-plugin",
|
||||
"androidx.benchmark:benchmark-junit4",
|
||||
"androidx.biometric:biometric",
|
||||
"androidx.browser:browser",
|
||||
"androidx.cardview:cardview",
|
||||
"androidx.collection:collection",
|
||||
"androidx.concurrent:concurrent-futures",
|
||||
"androidx.constraintlayout:constraintlayout",
|
||||
"androidx.constraintlayout:constraintlayout-solver",
|
||||
"androidx.contentpager:contentpager",
|
||||
"androidx.coordinatorlayout:coordinatorlayout",
|
||||
"androidx.core:core",
|
||||
"androidx.cursoradapter:cursoradapter",
|
||||
"androidx.customview:customview",
|
||||
"androidx.databinding:databinding-adapters",
|
||||
"androidx.databinding:databinding-common",
|
||||
"androidx.databinding:databinding-compiler",
|
||||
"androidx.databinding:databinding-compiler-common",
|
||||
"androidx.databinding:databinding-runtime",
|
||||
"androidx.databinding:viewbinding",
|
||||
"androidx.documentfile:documentfile",
|
||||
"androidx.drawerlayout:drawerlayout",
|
||||
"androidx.dynamicanimation:dynamicanimation",
|
||||
"androidx.emoji:emoji",
|
||||
"androidx.emoji:emoji-appcompat",
|
||||
"androidx.emoji:emoji-bundled",
|
||||
"androidx.enterprise:enterprise-feedback",
|
||||
"androidx.enterprise:enterprise-feedback-testing",
|
||||
"androidx.exifinterface:exifinterface",
|
||||
"androidx.fragment:fragment",
|
||||
"androidx.fragment:fragment-testing",
|
||||
"androidx.gridlayout:gridlayout",
|
||||
"androidx.heifwriter:heifwriter",
|
||||
"androidx.interpolator:interpolator",
|
||||
"androidx.leanback:leanback",
|
||||
"androidx.leanback:leanback-preference",
|
||||
"androidx.legacy:legacy-preference-v14",
|
||||
"androidx.legacy:legacy-support-core-ui",
|
||||
"androidx.legacy:legacy-support-core-utils",
|
||||
"androidx.legacy:legacy-support-v13",
|
||||
"androidx.legacy:legacy-support-v4",
|
||||
"androidx.lifecycle:lifecycle-common",
|
||||
"androidx.lifecycle:lifecycle-common-java8",
|
||||
"androidx.lifecycle:lifecycle-compiler",
|
||||
"androidx.lifecycle:lifecycle-extensions",
|
||||
"androidx.lifecycle:lifecycle-livedata",
|
||||
"androidx.lifecycle:lifecycle-livedata-core",
|
||||
"androidx.lifecycle:lifecycle-process",
|
||||
"androidx.lifecycle:lifecycle-reactivestreams",
|
||||
"androidx.lifecycle:lifecycle-runtime",
|
||||
"androidx.lifecycle:lifecycle-service",
|
||||
"androidx.lifecycle:lifecycle-viewmodel",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-savedstate",
|
||||
"androidx.loader:loader",
|
||||
"androidx.localbroadcastmanager:localbroadcastmanager",
|
||||
"androidx.media2:media2-common",
|
||||
"androidx.media2:media2-exoplayer",
|
||||
"androidx.media2:media2-player",
|
||||
"androidx.media2:media2-session",
|
||||
"androidx.media2:media2-widget",
|
||||
"androidx.media:media",
|
||||
"androidx.mediarouter:mediarouter",
|
||||
"androidx.multidex:multidex",
|
||||
"androidx.multidex:multidex-instrumentation",
|
||||
"androidx.navigation:navigation-common",
|
||||
"androidx.navigation:navigation-dynamic-features-fragment",
|
||||
"androidx.navigation:navigation-dynamic-features-runtime",
|
||||
"androidx.navigation:navigation-fragment",
|
||||
"androidx.navigation:navigation-runtime",
|
||||
"androidx.navigation:navigation-safe-args-generator",
|
||||
"androidx.navigation:navigation-safe-args-gradle-plugin",
|
||||
"androidx.navigation:navigation-testing",
|
||||
"androidx.navigation:navigation-ui",
|
||||
"androidx.paging:paging-common",
|
||||
"androidx.paging:paging-runtime",
|
||||
"androidx.paging:paging-rxjava2",
|
||||
"androidx.palette:palette",
|
||||
"androidx.percentlayout:percentlayout",
|
||||
"androidx.preference:preference",
|
||||
"androidx.print:print",
|
||||
"androidx.recommendation:recommendation",
|
||||
"androidx.recyclerview:recyclerview",
|
||||
"androidx.recyclerview:recyclerview-selection",
|
||||
"androidx.room:room-common",
|
||||
"androidx.room:room-compiler",
|
||||
"androidx.room:room-guava",
|
||||
"androidx.room:room-migration",
|
||||
"androidx.room:room-runtime",
|
||||
"androidx.room:room-rxjava2",
|
||||
"androidx.room:room-testing",
|
||||
"androidx.savedstate:savedstate",
|
||||
"androidx.sharetarget:sharetarget",
|
||||
"androidx.slice:slice-builders",
|
||||
"androidx.slice:slice-core",
|
||||
"androidx.slice:slice-view",
|
||||
"androidx.slidingpanelayout:slidingpanelayout",
|
||||
"androidx.sqlite:sqlite",
|
||||
"androidx.sqlite:sqlite-framework",
|
||||
"androidx.swiperefreshlayout:swiperefreshlayout",
|
||||
"androidx.test:core",
|
||||
"androidx.test.espresso:espresso-accessibility",
|
||||
"androidx.test.espresso:espresso-contrib",
|
||||
"androidx.test.espresso:espresso-core",
|
||||
"androidx.test.espresso:espresso-idling-resource",
|
||||
"androidx.test.espresso:espresso-intents",
|
||||
"androidx.test.espresso:espresso-remote",
|
||||
"androidx.test.espresso:espresso-web",
|
||||
"androidx.test.espresso.idling:idling-concurrent",
|
||||
"androidx.test.espresso.idling:idling-net",
|
||||
"androidx.test.ext:junit",
|
||||
"androidx.test.ext:truth",
|
||||
"androidx.test.janktesthelper:janktesthelper",
|
||||
"androidx.test:monitor",
|
||||
"androidx.test:rules",
|
||||
"androidx.test:runner",
|
||||
"androidx.test.uiautomator:uiautomator",
|
||||
"androidx.transition:transition",
|
||||
"androidx.tvprovider:tvprovider",
|
||||
"androidx.vectordrawable:vectordrawable",
|
||||
"androidx.vectordrawable:vectordrawable-animated",
|
||||
"androidx.versionedparcelable:versionedparcelable",
|
||||
"androidx.viewpager2:viewpager2",
|
||||
"androidx.viewpager:viewpager",
|
||||
"androidx.wear:wear",
|
||||
"androidx.webkit:webkit",
|
||||
"androidx.work:work-gcm",
|
||||
"androidx.work:work-runtime",
|
||||
"androidx.work:work-rxjava2",
|
||||
"androidx.work:work-testing"
|
||||
]
|
||||
}
|
||||
},
|
||||
"android-dev-ext.subscriptionKey": {
|
||||
"scope": "application",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"android-dev-ext.trace": {
|
||||
"scope": "resource",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable diagnostic trace logging in the extension output."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "android-dev-ext.view_logcat",
|
||||
@@ -220,22 +406,24 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "cd langserver && npm install && cd ..",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"long": "^4.0.0",
|
||||
"unzipper": "^0.10.10",
|
||||
"uuid": "^3.3.2",
|
||||
"vscode-debugadapter": "^1.40.0",
|
||||
"vscode-debugprotocol": "^1.40.0",
|
||||
"ws": "^7.1.2",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.27"
|
||||
"long": "4.0.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "8.2.0",
|
||||
"vscode-debugadapter": "1.40.0",
|
||||
"vscode-debugprotocol": "1.40.0",
|
||||
"vscode-languageclient": "6.1.3",
|
||||
"ws": "7.3.0",
|
||||
"xmldom": "0.1.31",
|
||||
"xpath": "0.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.12.5",
|
||||
"@types/vscode": "1.24.0",
|
||||
"@types/vscode": "^1.24.0",
|
||||
"eslint": "^5.9.0",
|
||||
"mocha": "^5.2.0",
|
||||
"typescript": "^3.8.3"
|
||||
|
||||
@@ -17,6 +17,11 @@ class APKFileInfo {
|
||||
*/
|
||||
file_data = null;
|
||||
|
||||
/**
|
||||
* The size of the APK file (in bytes)
|
||||
*/
|
||||
file_size = 0;
|
||||
|
||||
/**
|
||||
* last modified time of the APK file (in ms)
|
||||
*/
|
||||
@@ -67,6 +72,7 @@ class APKFileInfo {
|
||||
// read the APK file contents
|
||||
try {
|
||||
result.file_data = await readFile(args.apkFile);
|
||||
result.file_size = result.file_data.length;
|
||||
} catch(err) {
|
||||
throw new Error(`APK read error. ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
// node and external modules
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const uuidv4 = require('uuid').v4;
|
||||
|
||||
// our stuff
|
||||
const { ADBClient } = require('./adbclient');
|
||||
@@ -20,6 +21,7 @@ const { checkADBStarted, getAndroidSourcesFolder } = require('./utils/android');
|
||||
const { D, initLogToClient, onMessagePrint } = require('./utils/print');
|
||||
const { hasValidSourceFileExtension } = require('./utils/source-file');
|
||||
const { VariableManager } = require('./variable-manager');
|
||||
const analytics = require('../langserver/analytics');
|
||||
|
||||
class AndroidDebugSession extends DebugSession {
|
||||
|
||||
@@ -107,7 +109,13 @@ class AndroidDebugSession extends DebugSession {
|
||||
*/
|
||||
this.debug_mode = null;
|
||||
|
||||
// this debugger uses one-based lines and columns
|
||||
this.terminate_reason = '';
|
||||
|
||||
this.session_id = uuidv4();
|
||||
this.session_start = new Date();
|
||||
analytics.init();
|
||||
|
||||
// this debugger uses one-based lines and columns
|
||||
this.setDebuggerLinesStartAt1(true);
|
||||
this.setDebuggerColumnsStartAt1(true);
|
||||
|
||||
@@ -338,12 +346,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
// "null" is returned from the device picker if there's an error or if the
|
||||
// user cancels.
|
||||
D('targetDevice === "null"');
|
||||
this.terminate_reason = "null-targetdevice";
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.processId) {
|
||||
this.LOG(`Attach failed: Missing "processId" property in launch.json`);
|
||||
this.terminate_reason = "no-processid";
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -353,6 +363,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// - a JSON object returned from the process picker (contains the target device and process ID),
|
||||
let attach_info = this.extractPidAndTargetDevice(args.processId);
|
||||
if (!attach_info) {
|
||||
this.terminate_reason = "null-attachinfo";
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -370,6 +381,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// wow, we really didn't make it very far...
|
||||
this.LOG(err.message);
|
||||
this.LOG('Check the "appSrcRoot" entries in launch.json');
|
||||
this.terminate_reason = `init-exception: ${err.message}`;
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -417,6 +429,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.LOG(`Debugger attached`);
|
||||
await this.dbgr.resume();
|
||||
|
||||
analytics.event('debug-started', {
|
||||
dbg_session_id: this.session_id,
|
||||
dbg_start: this.session_start.toLocaleTimeString(),
|
||||
dbg_tz: this.session_start.getTimezoneOffset(),
|
||||
dbg_kind: 'attach',
|
||||
dbg_device_api: this.device_api_level,
|
||||
dbg_emulator: /^emulator/.test(this._device.serial),
|
||||
})
|
||||
} catch(e) {
|
||||
//this.performDisconnect();
|
||||
// exceptions use message, adbclient uses msg
|
||||
@@ -428,6 +448,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
|
||||
}
|
||||
// tell the client we're done
|
||||
this.terminate_reason = `start-exception: ${e.message||e.msg}`;
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
}
|
||||
}
|
||||
@@ -465,6 +486,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// "null" is returned from the device picker if there's an error or if the
|
||||
// user cancels.
|
||||
D('targetDevice === "null"');
|
||||
this.terminate_reason = "null-targetdevice";
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -481,6 +503,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// we don't allow both amStartArgs and launchActivity to be specified (the launch activity must be included in amStartArgs)
|
||||
if (args.amStartArgs && args.launchActivity) {
|
||||
this.LOG('amStartArgs and launchActivity options cannot both be specified in the launch configuration.');
|
||||
this.terminate_reason = "amStartArgs+launchActivity";
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -501,6 +524,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// wow, we really didn't make it very far...
|
||||
this.LOG(err.message);
|
||||
this.LOG('Check the "appSrcRoot" and "apkFile" entries in launch.json');
|
||||
this.terminate_reason = `init-exception: ${err.message}`;
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
return;
|
||||
}
|
||||
@@ -558,6 +582,17 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(response);
|
||||
await this.dbgr.resume();
|
||||
|
||||
analytics.event('debug-started', {
|
||||
dbg_session_id: this.session_id,
|
||||
dbg_start: this.session_start.toLocaleTimeString(),
|
||||
dbg_tz: this.session_start.getTimezoneOffset(),
|
||||
dbg_kind: 'debug',
|
||||
dbg_device_api: this.device_api_level,
|
||||
dbg_emulator: /^emulator/.test(this._device.serial),
|
||||
dbg_apk_size: this.apk_file_info.file_size,
|
||||
dbg_pkg_name: this.apk_file_info.manifest.package || '',
|
||||
})
|
||||
|
||||
this.LOG('Application started');
|
||||
} catch(e) {
|
||||
// exceptions use message, adbclient uses msg
|
||||
@@ -569,6 +604,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
|
||||
}
|
||||
// tell the client we're done
|
||||
this.terminate_reason = `start-exception: ${e.message||e.msg}`;
|
||||
this.sendEvent(new TerminatedEvent(false));
|
||||
}
|
||||
}
|
||||
@@ -748,6 +784,11 @@ class AndroidDebugSession extends DebugSession {
|
||||
async disconnectRequest(response) {
|
||||
D('disconnectRequest');
|
||||
this._isDisconnecting = true;
|
||||
analytics.event('debug-end', {
|
||||
dbg_session_id: this.session_id,
|
||||
dbg_elapsed: Math.trunc((Date.now() - this.session_start.getTime())/1e3),
|
||||
dbg_term_reason: this.terminate_reason,
|
||||
});
|
||||
if (this.debuggerAttached) {
|
||||
try {
|
||||
if (this.debug_mode === 'launch') {
|
||||
|
||||
@@ -51,7 +51,7 @@ class RootExpression extends ParsedExpression {
|
||||
class TypeCastExpression extends ParsedExpression {
|
||||
/**
|
||||
*
|
||||
* @param {string} cast_type
|
||||
* @param {ParsedExpression} cast_type
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(cast_type, rhs) {
|
||||
@@ -87,6 +87,18 @@ class UnaryOpExpression extends ParsedExpression {
|
||||
}
|
||||
}
|
||||
|
||||
class IncOpExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {'e++'|'e--'|'++e'|'--e'} which
|
||||
* @param {ParsedExpression} expression
|
||||
*/
|
||||
constructor(which, expression) {
|
||||
super();
|
||||
this.which = which;
|
||||
this.expression = expression;
|
||||
}
|
||||
}
|
||||
|
||||
class TernaryExpression extends ParsedExpression {
|
||||
|
||||
/**
|
||||
@@ -131,6 +143,48 @@ class MemberExpression extends QualifierExpression {
|
||||
}
|
||||
}
|
||||
|
||||
class BracketedExpression extends ParsedExpression {
|
||||
constructor(expression, qualified_terms) {
|
||||
super();
|
||||
this.expression = expression;
|
||||
this.qualified_terms = qualified_terms;
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayLiteralExpression extends ParsedExpression {
|
||||
elements = [];
|
||||
}
|
||||
|
||||
class ParsedNewExpression extends ParsedExpression {}
|
||||
|
||||
class NewObjectExpression extends ParsedNewExpression {
|
||||
/**
|
||||
* @param {RootExpression} ctr_call
|
||||
* @param {QualifierExpression[]} post_ctr_qualifiers
|
||||
* @param {string} method_body
|
||||
*/
|
||||
constructor(ctr_call, post_ctr_qualifiers, method_body) {
|
||||
super();
|
||||
this.ctr_call = ctr_call;
|
||||
this.qualified_terms = post_ctr_qualifiers;
|
||||
this.method_body = method_body;
|
||||
}
|
||||
}
|
||||
|
||||
class NewArrayExpression extends ParsedNewExpression {
|
||||
/**
|
||||
* @param {RootExpression} type
|
||||
* @param {ArrayIndexExpression[]} arrdim_initers
|
||||
* @param {QualifierExpression[]} post_ctr_qualifiers
|
||||
*/
|
||||
constructor(type, arrdim_initers, post_ctr_qualifiers) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.arrdim_initers = arrdim_initers;
|
||||
this.post_ctr_qualifiers = post_ctr_qualifiers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove characters from the expression followed by any leading whitespace/comments
|
||||
* @param {ExpressionText} e
|
||||
@@ -214,17 +268,21 @@ function parseBracketOrCastExpression(e) {
|
||||
if (!strip(e, ')')) {
|
||||
return null;
|
||||
}
|
||||
if (res instanceof RootExpression) {
|
||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) {
|
||||
// primitive typecast
|
||||
const castexpr = parse_expression_term(e);
|
||||
if (!castexpr) {
|
||||
return null;
|
||||
}
|
||||
res = new TypeCastExpression(res.root_term, castexpr);
|
||||
// note - a bracketed expression followed by another bracketed expression is assumed to be a cast:
|
||||
// double d = (double)(float)5; - is ok
|
||||
// XYZ xyz = (new XYZ)(1,2,3); - nope
|
||||
// - this will still need to be resolved for +/- e.g (int)+5 vs (some.field)+5
|
||||
if (/^[\w"'(!~]|^\.\d/.test(e.expr) && !/^!=/.test(e.expr)) {
|
||||
// typecast
|
||||
const castexpr = parse_expression(e);
|
||||
if (!castexpr) {
|
||||
return null;
|
||||
}
|
||||
return new TypeCastExpression(res, castexpr);
|
||||
}
|
||||
return res;
|
||||
|
||||
const qt = parse_qualified_terms(e);
|
||||
return new BracketedExpression(res, qt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,19 +303,124 @@ function parseUnaryExpression(e, unop) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @param {RootExpression} ctr
|
||||
* @param {QualifierExpression[]} ctr_qualifiers
|
||||
* @param {QualifierExpression[]} post_ctr_qualifiers
|
||||
*/
|
||||
function parseNewObjectExpression(e, ctr, ctr_qualifiers, post_ctr_qualifiers) {
|
||||
const ctr_call = new RootExpression(ctr.root_term, ctr.root_term_type, ctr_qualifiers);
|
||||
let method_body = null;
|
||||
if (!post_ctr_qualifiers.length) {
|
||||
// if there are no qualifiers following the constructor, look for an anonymous method body
|
||||
if (e.expr.startsWith('{')) {
|
||||
// don't parse it - just scan for the closing brace
|
||||
const brace_re = /\/\*[\d\D]*?\*\/|\/\/.*|".*?"|".*|'.'?|(\{)|(\})/g;
|
||||
let balance = 0, body_end = e.expr.length;
|
||||
for (let m; m = brace_re.exec(e.expr); ) {
|
||||
if (m[1]) balance++;
|
||||
else if (m[2] && (--balance === 0)) {
|
||||
body_end = m.index + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
method_body = e.expr.slice(0, body_end);
|
||||
strip(e, method_body.length);
|
||||
}
|
||||
}
|
||||
return new NewObjectExpression(ctr_call, post_ctr_qualifiers, method_body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @param {RootExpression} ctr
|
||||
* @param {Number} first_array_qualifier_idx
|
||||
*/
|
||||
function parseNewArrayExpression(e, ctr, first_array_qualifier_idx) {
|
||||
let arrdim_initers = [];
|
||||
let i = first_array_qualifier_idx;
|
||||
for (; i < ctr.qualified_terms.length; i++) {
|
||||
const term = ctr.qualified_terms[i];
|
||||
if (term instanceof ArrayIndexExpression) {
|
||||
arrdim_initers.push(term);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const type = new RootExpression(ctr.root_term, ctr.root_term_type, ctr.qualified_terms.slice(0, first_array_qualifier_idx));
|
||||
return new NewArrayExpression(type, arrdim_initers, ctr.qualified_terms.slice(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parseNewExpression(e) {
|
||||
const ctr = parse_expression_term(e);
|
||||
if (!(ctr instanceof RootExpression)) {
|
||||
return null;
|
||||
}
|
||||
let new_expression = null;
|
||||
ctr.qualified_terms.find((qt,idx) => {
|
||||
if (qt instanceof MethodCallExpression) {
|
||||
// new object contructor - split into constructor qualifiers and post-ctr-qualifiers
|
||||
const qualified_terms = ctr.qualified_terms.slice();
|
||||
const ctr_qualifiers = qualified_terms.splice(0, idx + 1);
|
||||
new_expression = parseNewObjectExpression(e, ctr, ctr_qualifiers, qualified_terms);
|
||||
return true;
|
||||
}
|
||||
if (qt instanceof ArrayIndexExpression) {
|
||||
// new array constructor
|
||||
// in java, multi-dimensional array constructors have priority over array accessors
|
||||
// e.g new int[2][1] - is a 2D array,
|
||||
// (new int[2])[1] - is the 1th element of a 1D array
|
||||
new_expression = parseNewArrayExpression(e, ctr, idx);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!new_expression) {
|
||||
// treat unqualified new expressions as object constructors with no parameters
|
||||
// eg. new XYZ === new XYZ()
|
||||
new_expression = parseNewObjectExpression(e, ctr, ctr.qualified_terms, []);
|
||||
}
|
||||
return new_expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parseArrayLiteral(e) {
|
||||
const arr = new ArrayLiteralExpression();
|
||||
if (!strip(e, '}')) {
|
||||
for (let element; ;) {
|
||||
if ((element = parse_expression(e)) === null) {
|
||||
return null;
|
||||
}
|
||||
arr.elements.push(element);
|
||||
if (strip(e, ',')) continue;
|
||||
if (strip(e, '}')) break;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parse_expression_term(e) {
|
||||
if (e.expr[0] === '(') {
|
||||
return parseBracketOrCastExpression(new ExpressionText(e.expr));
|
||||
const subexpr = new ExpressionText(e.expr);
|
||||
const bexpr = parseBracketOrCastExpression(subexpr);
|
||||
e.expr = subexpr.expr;
|
||||
return bexpr;
|
||||
}
|
||||
const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||
if (unop) {
|
||||
return parseUnaryExpression(e, unop[0]);
|
||||
}
|
||||
const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'];
|
||||
const root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
let root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
if (!root_term) {
|
||||
return null;
|
||||
}
|
||||
@@ -267,10 +430,6 @@ function parse_expression_term(e) {
|
||||
if (qualified_terms === null) {
|
||||
return null;
|
||||
}
|
||||
// the root term is not allowed to be a method call
|
||||
if (qualified_terms[0] instanceof MethodCallExpression) {
|
||||
return null;
|
||||
}
|
||||
return new RootExpression(root_term[0], root_term_type, qualified_terms);
|
||||
}
|
||||
|
||||
@@ -278,16 +437,45 @@ function parse_expression_term(e) {
|
||||
* @param {string} s
|
||||
*/
|
||||
function getBinaryOperator(s) {
|
||||
const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
const binary_op_match = s.match(/^([!=/%*^+-]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
return binary_op_match ? binary_op_match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @param {ExpressionText|string} e
|
||||
* @returns {ParsedExpression}
|
||||
*/
|
||||
function parse_expression(e) {
|
||||
if (typeof e === 'string') {
|
||||
e = new ExpressionText(e);
|
||||
}
|
||||
const newop = e.expr.match(/^new\b/);
|
||||
if (newop) {
|
||||
strip(e, 3);
|
||||
return parseNewExpression(e);
|
||||
}
|
||||
const arrayinit = e.expr.match(/^\{/);
|
||||
if (arrayinit) {
|
||||
strip(e, 1);
|
||||
return parseArrayLiteral(e);
|
||||
}
|
||||
const prefix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?=[a-zA-Z_])/);
|
||||
if (prefix_incdec) {
|
||||
strip(e, 2);
|
||||
}
|
||||
let res = parse_expression_term(e);
|
||||
if (prefix_incdec) {
|
||||
res = new IncOpExpression(e.expr[1] ? '++e' : '--e', res);
|
||||
}
|
||||
|
||||
const postfix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?![+-])/);
|
||||
if (postfix_incdec) {
|
||||
if (prefix_incdec) {
|
||||
return null;
|
||||
}
|
||||
strip(e, 2);
|
||||
res = new IncOpExpression(e.expr[1] ? 'e++' : 'e--', res);
|
||||
}
|
||||
|
||||
for (; ;) {
|
||||
const binary_operator = getBinaryOperator(e.expr);
|
||||
@@ -327,6 +515,8 @@ module.exports = {
|
||||
ExpressionText,
|
||||
MemberExpression,
|
||||
MethodCallExpression,
|
||||
NewArrayExpression,
|
||||
NewObjectExpression,
|
||||
parse_expression,
|
||||
ParsedExpression,
|
||||
QualifierExpression,
|
||||
|
||||
Reference in New Issue
Block a user