mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb6c78070c | ||
|
|
0bc2ab528c | ||
|
|
ec798e17f4 | ||
|
|
ba05c54e8f | ||
|
|
7bfb57121f | ||
|
|
c10fc42661 | ||
|
|
e3d835b563 | ||
|
|
456c4f935f | ||
|
|
33dd93da0c | ||
|
|
05b3877bcb | ||
|
|
d15a7bd911 | ||
|
|
8dbbfa8344 | ||
|
|
669ed81f39 | ||
|
|
bfd55354c7 | ||
|
|
83eda790be | ||
|
|
d064b9a3f4 | ||
|
|
6439e1b8b7 | ||
|
|
a4ce09d309 | ||
|
|
1535f133d9 | ||
|
|
44d887dd6c | ||
|
|
9aeca6b96b | ||
|
|
0672e54401 | ||
|
|
f92f247ef6 | ||
|
|
133b7061b2 | ||
|
|
7e958620a8 | ||
|
|
989de8254a | ||
|
|
8f2f7d2fd4 | ||
|
|
99544b527d |
@@ -1,23 +1,28 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
"plugins": ["jsdoc"],
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 13,
|
||||
"ecmaFeatures": {
|
||||
"jsx": false
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-const-assign": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-unreachable": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"constructor-super": "warn",
|
||||
"valid-typeof": "warn"
|
||||
}
|
||||
}
|
||||
"sourceType": "commonjs"
|
||||
},
|
||||
"rules": {
|
||||
"no-const-assign": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-unreachable": "warn",
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{ "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"constructor-super": "warn",
|
||||
"valid-typeof": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@@ -8,15 +8,30 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||
"stopOnEntry": false
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Server",
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Language Server",
|
||||
"port": 6009,
|
||||
"cwd": "${workspaceFolder}/langserver",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Debugger Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"program": "${workspaceRoot}/src/debugMain.js",
|
||||
"args": [ "--server=4711" ]
|
||||
"args": [ "--server=4711" ],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Tests",
|
||||
@@ -24,13 +39,16 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ],
|
||||
"stopOnEntry": false
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Extension + Server",
|
||||
"configurations": [ "Launch Extension", "Server" ]
|
||||
"name": "Extension + Debugger",
|
||||
"configurations": [ "Launch Extension", "Debugger Server" ]
|
||||
},
|
||||
{
|
||||
"name": "Debug Language Server",
|
||||
"configurations": [ "Launch Extension", "Attach to Language Server" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Change Log
|
||||
|
||||
### version 1.3.2
|
||||
* Update analytics library
|
||||
* Update lodash version - security advisory https://www.npmjs.com/advisories/1523
|
||||
|
||||
### version 1.3.0
|
||||
* Support `ADB_SERVER_SOCKET`, `ANDROID_ADB_SERVER_ADDRESS` & `ANDROID_ADB_SERVER_PORT` env vars when connecting to ADB.
|
||||
* Replace `adbPort` configuration option with a new `adbSocket` value to allow ADB server host to be overidden. (`adbPort` is now deprecated).
|
||||
* Allow the JDWP local port to be fixed using a new `jdwpPort` configuration option.
|
||||
|
||||
### version 1.2.1
|
||||
* Java Intellisense: automatically import dependencies of AndroidX libraries.
|
||||
* Debugger: Warn about open instances of Android Studio
|
||||
|
||||
### 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.
|
||||
* A "${command:PickAndroidDevice}" value allows a deployment device to be chosen during each launch
|
||||
* Watch and repl expressions now support format specifiers
|
||||
* Small bug fixes and performance improvements
|
||||
|
||||
### version 1.0.0
|
||||
* Update extension to support minimum version of node v10
|
||||
* refactoring and improvement of type-checking using jsdocs
|
||||
|
||||
### version 0.8.0
|
||||
* Try to extract Android manifest directly from APK
|
||||
* Added `manifestFile` launch configuration property
|
||||
* Allow `pm install` arguments to be customised as a launch configuration property
|
||||
* Document `launchActivity` launch configuration property
|
||||
* Fix critical security advisory https://www.npmjs.com/advisories/1118
|
||||
|
||||
### version 0.7.1
|
||||
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
|
||||
|
||||
### version 0.7.0
|
||||
* Fix logcat not displaying
|
||||
* Fix breakpoints not triggering on Windows
|
||||
|
||||
139
README.md
139
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,41 +21,84 @@ 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
|
||||
|
||||
This extension allows you to debug your App by creating a new Android configuration in `launch.json`.
|
||||
The following settings are used to configure the debugger:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session
|
||||
// configuration type, request and name. "launch" is used to deploy the app
|
||||
// to your device and start a debugging session.
|
||||
"type": "android",
|
||||
"request": "launch",
|
||||
"name": "Launch App",
|
||||
|
||||
// Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)
|
||||
// Location of the App source files. This value must point to the root of
|
||||
// your App source tree (containing AndroidManifest.xml).
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
|
||||
// Fully qualified path to the built APK (Android Application Package)
|
||||
// Fully qualified path to the built APK (Android Application Package).
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
||||
|
||||
// Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037
|
||||
"adbPort": 5037,
|
||||
// `host:port` configuration for connecting to the ADB (Android Debug Bridge) server instance.
|
||||
// Default: localhost:5037
|
||||
"adbSocket": "localhost:5037",
|
||||
|
||||
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
|
||||
// Automatically launch 'adb start-server' if not already started.
|
||||
// Default: true
|
||||
"autoStartADB": true,
|
||||
|
||||
// Launch behaviour if source files have been saved after the APK was built.
|
||||
// One of: [ ignore warn stop ]. Default: warn
|
||||
"staleBuild": "warn",
|
||||
|
||||
// Target Device ID (as indicated by 'adb devices').
|
||||
// Use this to specify which device is used for deployment
|
||||
// when multiple devices are connected.
|
||||
"targetDevice": "",
|
||||
|
||||
// Fully qualified path to the AndroidManifest.xml file compiled into the APK.
|
||||
// Default: "${appSrcRoot}/AndroidManifest.xml"
|
||||
"manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml",
|
||||
|
||||
// Custom arguments passed to the Android package manager to install the app.
|
||||
// Run 'adb shell pm' to show valid arguments. Default: ["-r"]
|
||||
"pmInstallArgs": ["-r"],
|
||||
|
||||
// Custom arguments passed to the Android application manager to start the app.
|
||||
// Run `adb shell am` to show valid arguments.
|
||||
// Note that `-D` is required to enable debugging.
|
||||
"amStartArgs": [
|
||||
"-D",
|
||||
"--activity-brought-to-front",
|
||||
"-a android.intent.action.MAIN",
|
||||
"-c android.intent.category.LAUNCHER",
|
||||
"-n package.name/launch.activity"
|
||||
],
|
||||
|
||||
// Manually specify the activity to run when the app is started. This option is
|
||||
// mutually exclusive with "amStartArgs".
|
||||
"launchActivity": ".MainActivity",
|
||||
|
||||
// Time in milliseconds to wait after launching an app before attempting to attach
|
||||
// the debugger. Default: 1000ms
|
||||
"postLaunchPause": 1000,
|
||||
|
||||
// Set to true to output debugging logs for diagnostics.
|
||||
"trace": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Building your app automatically
|
||||
|
||||
@@ -69,6 +115,7 @@ Add a `preLaunchTask` item to the launch configuration:
|
||||
"request": "launch",
|
||||
"name": "App Build & Launch",
|
||||
"preLaunchTask": "run gradle",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -83,12 +130,80 @@ Add a new task to run the build command:
|
||||
"label": "run gradle",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/gradlew",
|
||||
"args": ["assembleDebug"]
|
||||
"args": [
|
||||
"assembleDebug"
|
||||
],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
The specifiers work with the same syntax used in Visual Studio.
|
||||
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
|
||||
|
||||
```
|
||||
123 123
|
||||
123,x 0x0000007b
|
||||
123,xb 0000007b
|
||||
123,X 0x0000007B
|
||||
123,o 000000000173
|
||||
123,b 0b00000000000000000000000001111011
|
||||
123,bb 00000000000000000000000001111011
|
||||
123,c '{'
|
||||
"one\ntwo" "one\ntwo"
|
||||
"one\ntwo",sb one\ntwo
|
||||
"one\ntwo",! one
|
||||
two
|
||||
```
|
||||
|
||||
You can also apply the specifiers to object and array instances to format fields and elements:
|
||||
```
|
||||
arr,x int[3]
|
||||
[0] 0x00000001
|
||||
[1] 0x00000002
|
||||
[1] 0x00000003
|
||||
```
|
||||
|
||||
|
||||
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
|
||||
|
||||
|
||||
## Powered by coffee
|
||||
|
||||
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
|
||||
can support it by [buying me a coffee](https://www.buymeacoffee.com/adelphes).
|
||||
|
||||
If you use ApplePay or Google Pay, you can scan the code with your phone camera:
|
||||
|
||||

|
||||
|
||||
Every coffee makes a difference, so thanks for adding your support.
|
||||
|
||||
## Questions / Problems
|
||||
|
||||
|
||||
182
extension.js
182
extension.js
@@ -1,48 +1,188 @@
|
||||
// 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 state = require('./src/state');
|
||||
const { selectAndroidProcessID } = require('./src/process-attach');
|
||||
const { selectTargetDevice } = require('./src/utils/device');
|
||||
|
||||
function getADBPort() {
|
||||
var defaultPort = 5037;
|
||||
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
|
||||
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
|
||||
return adbPort;
|
||||
return defaultPort;
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
* @param {string} uid
|
||||
* @param {number} session_id
|
||||
* @param {*} vscode_props
|
||||
*/
|
||||
async function createLanguageClient(context, uid, session_id, vscode_props) {
|
||||
// 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());
|
||||
|
||||
// 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,
|
||||
uid,
|
||||
session_id,
|
||||
vscode_props,
|
||||
initialSettings: config,
|
||||
sourceFiles,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// this method is called when your extension is activated
|
||||
// your extension is activated the very first time the command is executed
|
||||
/** @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);
|
||||
|
||||
// logcat connections require the (fake) websocket proxy to be up
|
||||
// - take the ADB port from launch.json
|
||||
const wsproxyserver = require('./src/wsproxy').proxy.Server(6037, getADBPort());
|
||||
const { uid } = analytics.getIDs(context);
|
||||
const session_id = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
const vscode_props = {
|
||||
appName: vscode.env.appName,
|
||||
language: vscode.env.language,
|
||||
shell: vscode.env.shell,
|
||||
uiKind: vscode.env.uiKind,
|
||||
vscode_version: vscode.version,
|
||||
}
|
||||
analytics.init(undefined, uid, session_id, '', package_json, vscode_props, 'extension-start');
|
||||
|
||||
createLanguageClient(context, uid, session_id, vscode_props).then(client => {
|
||||
languageClient = client;
|
||||
refreshLanguageServerEnabledState();
|
||||
});
|
||||
|
||||
// The commandId parameter must match the command field in package.json
|
||||
var disposables = [
|
||||
const disposables = [
|
||||
// add the view logcat handler
|
||||
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
|
||||
openLogcatWindow(vscode);
|
||||
}),
|
||||
// watch for changes in the launch config
|
||||
// add the device picker handler - used to choose a target device
|
||||
vscode.commands.registerCommand('PickAndroidDevice', async (launchConfig) => {
|
||||
// if the config has both PickAndroidDevice and PickAndroidProcess, ignore this
|
||||
// request as PickAndroidProcess already includes chooosing a device...
|
||||
if (launchConfig && launchConfig.processId === '${command:PickAndroidProcess}') {
|
||||
return '';
|
||||
}
|
||||
const device = await selectTargetDevice(vscode, "Launch", { alwaysShow:true });
|
||||
// the debugger requires a string value to be returned
|
||||
return JSON.stringify(device);
|
||||
}),
|
||||
// add the process picker handler - used to choose a PID to attach to
|
||||
vscode.commands.registerCommand('PickAndroidProcess', async (launchConfig) => {
|
||||
// if the config has a targetDevice specified, use it instead of choosing a device...
|
||||
let target_device = '';
|
||||
if (launchConfig && typeof launchConfig.targetDevice === 'string') {
|
||||
target_device = launchConfig.targetDevice;
|
||||
}
|
||||
const explicit_pick_device = target_device === '${command:PickAndroidDevice}';
|
||||
if (!target_device || explicit_pick_device) {
|
||||
// no targetDevice (or it's set to ${command:PickAndroidDevice})
|
||||
const device = await selectTargetDevice(vscode, 'Attach', { alwaysShow: explicit_pick_device });
|
||||
if (!device) {
|
||||
return JSON.stringify({status: 'cancelled'});
|
||||
}
|
||||
target_device = device.serial;
|
||||
}
|
||||
const o = await selectAndroidProcessID(vscode, target_device);
|
||||
// the debugger requires a string value to be returned
|
||||
return JSON.stringify(o);
|
||||
}),
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
wsproxyserver.setADBPort(getADBPort());
|
||||
})
|
||||
// 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());
|
||||
}),
|
||||
];
|
||||
|
||||
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
||||
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
||||
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
|
||||
}
|
||||
|
||||
exports.activate = activate;
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
function deactivate() {
|
||||
analytics.event('extension-deactivate');
|
||||
}
|
||||
exports.deactivate = deactivate;
|
||||
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
|
||||
BIN
images/bmac-code.png
Normal file
BIN
images/bmac-code.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
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-34.zip
Normal file
0
langserver/.library-cache/android-34.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
0
langserver/.library-cache/androidx-20200701.zip
Normal file
176
langserver/analytics.js
Normal file
176
langserver/analytics.js
Normal file
@@ -0,0 +1,176 @@
|
||||
const os = require('os');
|
||||
const uuid = require('uuid').v4;
|
||||
let client;
|
||||
/** @type {string} */
|
||||
let uid;
|
||||
/** @type {string} */
|
||||
let did = uuid();
|
||||
/** @type {number} */
|
||||
let session_id;
|
||||
/** @type {Map<string,[number,number]>} */
|
||||
const timeLabels = new Map();
|
||||
let session_start = Date.now();
|
||||
/** @type {string|Promise<string>} */
|
||||
let ip = '';
|
||||
let queued_events = null;
|
||||
let package_info = null;
|
||||
let vscode_info = null;
|
||||
|
||||
/**
|
||||
* @param {string} [t]
|
||||
* @param {string} u
|
||||
* @param {number} s
|
||||
* @param {string} ipaddr
|
||||
* @param {{name:string,version:string}} package_json
|
||||
* @param {*} vscode_props
|
||||
* @param {string} caller
|
||||
*/
|
||||
function init(t = '94635b4642d80407accd3739fa35bed6', u, s, ipaddr, package_json, vscode_props, caller) {
|
||||
if (client) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
client = require('@amplitude/node').init(t);
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
uid = u;
|
||||
session_id = s || Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
ip = ipaddr || (getCurrentIP()
|
||||
.catch(() => '')
|
||||
.then(res => ip = res));
|
||||
package_info = package_json;
|
||||
vscode_info = vscode_props;
|
||||
|
||||
if (!caller) {
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
event(caller, {
|
||||
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(),
|
||||
...vscode_props,
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentIP() {
|
||||
return new Promise((resolve, reject) => {
|
||||
require('https').get(
|
||||
Buffer.from('aHR0cHM6Ly91YTF4c3JhM2ZhLmV4ZWN1dGUtYXBpLmV1LXdlc3QtMi5hbWF6b25hd3MuY29tL3JlbA==','base64').toString(),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
res => resolve(res.headers['x-request-ip'])
|
||||
)
|
||||
.on('error', err => reject(err));
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @param {*} [properties]
|
||||
*/
|
||||
function event(eventName, properties) {
|
||||
if (!client || !eventName || (!uid && !did) || !ip) {
|
||||
return;
|
||||
}
|
||||
if (queued_events) {
|
||||
queued_events.push({eventName, properties});
|
||||
return;
|
||||
}
|
||||
if (ip instanceof Promise) {
|
||||
queued_events = [{eventName, properties}]
|
||||
ip.catch(() => {}).then(() => {
|
||||
const e = queued_events;
|
||||
queued_events = null;
|
||||
e.forEach(({eventName, properties}) => event(eventName, properties));
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
client.logEvent({
|
||||
event_type: eventName,
|
||||
user_id: uid,
|
||||
device_id: uid ? undefined : did,
|
||||
app_version: package_info.version,
|
||||
ip,
|
||||
language: vscode_info.language,
|
||||
os_name: process.platform,
|
||||
os_version: os.release(),
|
||||
session_id,
|
||||
event_properties: {
|
||||
session_length: Math.trunc((Date.now() - session_start) / 60e3),
|
||||
...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: '',
|
||||
};
|
||||
}
|
||||
let u = uid || (uid = context.globalState.get('mix-panel-id'));
|
||||
if (typeof u !== 'string' || u.length > 36) {
|
||||
u = uid = uuid();
|
||||
context.globalState.update('mix-panel-id', u);
|
||||
}
|
||||
return {
|
||||
uid: u,
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
522
langserver/completions.js
Normal file
522
langserver/completions.js
Normal file
@@ -0,0 +1,522 @@
|
||||
const { CEIType, ArrayType, PrimitiveType } = require('java-mti');
|
||||
const { getTypeInheritanceList } = require('./java/expression-resolver');
|
||||
const { 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');
|
||||
|
||||
/**
|
||||
* @typedef {import('vscode-languageserver').CompletionItem} CompletionItem
|
||||
* @typedef {import('java-mti').JavaType} JavaType
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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) => 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, // total count for this session
|
||||
comp_req_partial_count: (completionRequestCount % 25) || 25,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
285
langserver/document.js
Normal file
285
langserver/document.js
Normal file
@@ -0,0 +1,285 @@
|
||||
const { parse } = require('./java/body-parser');
|
||||
const { parseMethodBodies } = require('./java/validater');
|
||||
const { time, timeEnd, trace } = require('./logging');
|
||||
|
||||
/**
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
* @typedef {import('./java/source-types').SourceUnit} SourceUnit
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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-34.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;
|
||||
}
|
||||
30
langserver/library-cache.js
Normal file
30
langserver/library-cache.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 1. Download the latest platform and source
|
||||
* ./android-sdk/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install 'platforms;android-30'
|
||||
* ./android-sdk/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install 'sources;android-30'
|
||||
*
|
||||
* 2. Run this file, passing in the android API version
|
||||
* node library-cache.js android-30
|
||||
*
|
||||
* 3. To create the final shipped data, move the JSON into a 'cache' folder and zip
|
||||
* mkdir cache
|
||||
* mv android-30.json cache
|
||||
* zip -9 -r android-30.zip cache
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { createAndroidLibraryCacheFile } = require('java-mti/android-library');
|
||||
|
||||
/**
|
||||
* @param {`android-${number}`} api
|
||||
*/
|
||||
async function buildLibraryCache(api) {
|
||||
// `createAndroidLibraryCacheFile()` just creates the JSON (not the zipped version)
|
||||
const cache_filename = path.join(__dirname, '.library-cache', `${api}.json`);
|
||||
await createAndroidLibraryCacheFile(cache_filename, { api });
|
||||
}
|
||||
|
||||
const api = process.argv[2];
|
||||
if (!api) throw new Error('android api parameter expected');
|
||||
|
||||
buildLibraryCache(api);
|
||||
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;
|
||||
95
langserver/method-signatures.js
Normal file
95
langserver/method-signatures.js
Normal file
@@ -0,0 +1,95 @@
|
||||
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,
|
||||
methsig_req_partial_count: (methodsigRequestCount % 25) || 25,
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
707
langserver/package-lock.json
generated
Normal file
707
langserver/package-lock.json
generated
Normal file
@@ -0,0 +1,707 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langserver",
|
||||
"version": "1.0.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@amplitude/node": "^0.3.3",
|
||||
"java-mti": "adelphes/java-mti#ec164ac",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/node": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/node/-/node-0.3.3.tgz",
|
||||
"integrity": "sha512-Uzg4MRAuD053Ex67Iu2lm2GovnVte1uKI3q7CXlMCYZ9ylZmAkPbTnjg9OVyD4f+IiUfgK4p3bE7r9p7jqSDLA==",
|
||||
"dependencies": {
|
||||
"@amplitude/types": "^0.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/types": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-0.3.2.tgz",
|
||||
"integrity": "sha512-7+m7nhJMFGbpsppOUsCH8f4FOFyAxgKFuXkKknU/LP2CMYVjWEIoLTKKgaJPc2c8wXaK5KPXVetb8VeiGbuaGg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "13.13.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
|
||||
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
|
||||
"dependencies": {
|
||||
"buffers": "~0.1.1",
|
||||
"chainsaw": "~0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-indexof-polyfill": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
|
||||
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
|
||||
"dependencies": {
|
||||
"traverse": ">=0.3.0 <0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/fstream": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
|
||||
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"inherits": "~2.0.0",
|
||||
"mkdirp": ">=0.5 0",
|
||||
"rimraf": "2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/java-mti": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "git+ssh://git@github.com/adelphes/java-mti.git#ec164ace68267813a1ca9df18651b51e3f3f067d",
|
||||
"dependencies": {
|
||||
"unzipper": "0.10.11"
|
||||
}
|
||||
},
|
||||
"node_modules/listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dependencies": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
|
||||
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/unzipper": {
|
||||
"version": "0.10.11",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
|
||||
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
|
||||
"dependencies": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"vscode-languageserver-protocol": "^3.15.3"
|
||||
},
|
||||
"bin": {
|
||||
"installServerIntoExtension": "bin/installServerIntoExtension"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-protocol": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "8.2.0",
|
||||
"vscode-languageserver-types": "3.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/node": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/node/-/node-0.3.3.tgz",
|
||||
"integrity": "sha512-Uzg4MRAuD053Ex67Iu2lm2GovnVte1uKI3q7CXlMCYZ9ylZmAkPbTnjg9OVyD4f+IiUfgK4p3bE7r9p7jqSDLA==",
|
||||
"requires": {
|
||||
"@amplitude/types": "^0.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@amplitude/types": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-0.3.2.tgz",
|
||||
"integrity": "sha512-7+m7nhJMFGbpsppOUsCH8f4FOFyAxgKFuXkKknU/LP2CMYVjWEIoLTKKgaJPc2c8wXaK5KPXVetb8VeiGbuaGg=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "13.13.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
|
||||
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
|
||||
"dev": true
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.52",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="
|
||||
},
|
||||
"binary": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
|
||||
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
|
||||
"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": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
|
||||
},
|
||||
"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.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
|
||||
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
|
||||
},
|
||||
"buffers": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
|
||||
},
|
||||
"chainsaw": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
|
||||
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
|
||||
"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": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"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.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"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": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"java-mti": {
|
||||
"version": "git+ssh://git@github.com/adelphes/java-mti.git#ec164ace68267813a1ca9df18651b51e3f3f067d",
|
||||
"from": "java-mti@adelphes/java-mti#ec164ac",
|
||||
"requires": {
|
||||
"unzipper": "0.10.11"
|
||||
}
|
||||
},
|
||||
"listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
|
||||
},
|
||||
"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.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"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": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||
},
|
||||
"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": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"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": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="
|
||||
},
|
||||
"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.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||
"requires": {
|
||||
"vscode-jsonrpc": "8.2.0",
|
||||
"vscode-languageserver-types": "3.17.5"
|
||||
}
|
||||
},
|
||||
"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.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||
},
|
||||
"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": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
22
langserver/package.json
Normal file
22
langserver/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "langserver",
|
||||
"version": "1.0.4",
|
||||
"description": "Language server for Android development",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@amplitude/node": "^0.3.3",
|
||||
"java-mti": "adelphes/java-mti#ec164ac",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
302
langserver/server.js
Normal file
302
langserver/server.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
ProposedFeatures,
|
||||
DidChangeConfigurationNotification,
|
||||
TextDocumentSyncKind,
|
||||
} = require('vscode-languageserver');
|
||||
const fs = require('fs');
|
||||
|
||||
const { URI } = require('vscode-uri');
|
||||
|
||||
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
|
||||
|
||||
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 analytics = require('./analytics');
|
||||
const package_json = require('./package.json');
|
||||
|
||||
/**
|
||||
* @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
|
||||
* @typedef {import('java-mti').CEIType} CEIType
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.uid, startupOpts.session_id, '', package_json, startupOpts.vscode_props, 'langserver-start');
|
||||
|
||||
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();
|
||||
5304
package-lock.json
generated
5304
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
348
package.json
348
package.json
@@ -2,12 +2,12 @@
|
||||
"name": "android-dev-ext",
|
||||
"displayName": "Android",
|
||||
"description": "Android debugging support for VS Code",
|
||||
"version": "0.7.0",
|
||||
"version": "1.4.0",
|
||||
"publisher": "adelphes",
|
||||
"preview": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.8.0"
|
||||
"vscode": "^1.24.0"
|
||||
},
|
||||
"categories": [
|
||||
"Debuggers"
|
||||
@@ -18,7 +18,10 @@
|
||||
"theme": "dark"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:android-dev-ext.view_logcat"
|
||||
"onCommand:android-dev-ext.view_logcat",
|
||||
"onCommand:PickAndroidDevice",
|
||||
"onCommand:PickAndroidProcess",
|
||||
"onLanguage:java"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,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",
|
||||
@@ -50,10 +238,20 @@
|
||||
"launch": {
|
||||
"required": [
|
||||
"appSrcRoot",
|
||||
"apkFile",
|
||||
"adbPort"
|
||||
"apkFile"
|
||||
],
|
||||
"properties": {
|
||||
"amStartArgs": {
|
||||
"type": "array",
|
||||
"description": "Custom arguments to pass to the Android application manager to start the app. Run `adb shell am` to show valid arguments. Note that `-D` is required to enable debugging.\r\nBe careful using this option - you must specify the correct parameters or the app will not start.\r\n\r\nThis option is incompatible with the `launchActivity` option.",
|
||||
"default": [
|
||||
"-D",
|
||||
"--activity-brought-to-front",
|
||||
"-a android.intent.action.MAIN",
|
||||
"-c android.intent.category.LAUNCHER",
|
||||
"-n package.name/launch.activity"
|
||||
]
|
||||
},
|
||||
"appSrcRoot": {
|
||||
"type": "string",
|
||||
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
||||
@@ -66,12 +264,17 @@
|
||||
},
|
||||
"adbPort": {
|
||||
"type": "integer",
|
||||
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
|
||||
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.",
|
||||
"default": 5037
|
||||
},
|
||||
"adbSocket": {
|
||||
"type": "string",
|
||||
"description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"",
|
||||
"default": "localhost:5037"
|
||||
},
|
||||
"autoStartADB": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically launch 'adb start-server' if not already started. Default: true",
|
||||
"description": "Automatically attempt to launch 'adb start-server' if not already started. Default: true",
|
||||
"default": true
|
||||
},
|
||||
"callStackDisplaySize": {
|
||||
@@ -79,11 +282,38 @@
|
||||
"description": "Number of entries to display in call stack views (for locations outside of the project source). 0 shows the entire call stack. Default: 1",
|
||||
"default": 1
|
||||
},
|
||||
"jdwpPort": {
|
||||
"type": "integer",
|
||||
"description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0",
|
||||
"default": 0
|
||||
},
|
||||
"launchActivity": {
|
||||
"type": "string",
|
||||
"description": "Manually specify the activity to run when the app is started.",
|
||||
"default": ""
|
||||
},
|
||||
"logcatPort": {
|
||||
"type": "integer",
|
||||
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
|
||||
"default": 7038
|
||||
},
|
||||
"manifestFile": {
|
||||
"type": "string",
|
||||
"description": "Overrides the default location of AndroidManifest.xml",
|
||||
"default": "${workspaceRoot}/app/src/main/AndroidManifest.xml"
|
||||
},
|
||||
"pmInstallArgs": {
|
||||
"type": "array",
|
||||
"description": "APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: [\"-r\"]",
|
||||
"default": [
|
||||
"-r"
|
||||
]
|
||||
},
|
||||
"postLaunchPause": {
|
||||
"type": "number",
|
||||
"description": "Time in milliseconds to wait after launching an app before attempting to attach the debugger. Default: 1000",
|
||||
"default": 1000
|
||||
},
|
||||
"staleBuild": {
|
||||
"type": "string",
|
||||
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
|
||||
@@ -92,12 +322,55 @@
|
||||
"targetDevice": {
|
||||
"type": "string",
|
||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
|
||||
"default": ""
|
||||
"default": "${command:PickAndroidDevice}"
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to output debugging logs for diagnostics",
|
||||
"default": "false"
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"required": [
|
||||
"appSrcRoot",
|
||||
"processId"
|
||||
],
|
||||
"properties": {
|
||||
"appSrcRoot": {
|
||||
"type": "string",
|
||||
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
|
||||
"default": "${workspaceRoot}/app/src/main"
|
||||
},
|
||||
"adbPort": {
|
||||
"type": "integer",
|
||||
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.",
|
||||
"default": 5037
|
||||
},
|
||||
"adbSocket": {
|
||||
"type": "string",
|
||||
"description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"",
|
||||
"default": "localhost:5037"
|
||||
},
|
||||
"jdwpPort": {
|
||||
"type": "integer",
|
||||
"description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0",
|
||||
"default": 0
|
||||
},
|
||||
"processId": {
|
||||
"type": "string",
|
||||
"description": "PID of process to attach to.\n\"${command:PickAndroidProcess}\" will display a list of debuggable PIDs to choose from during launch.",
|
||||
"default": "${command:PickAndroidProcess}"
|
||||
},
|
||||
"targetDevice": {
|
||||
"type": "string",
|
||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used when multiple devices are connected.",
|
||||
"default": "${command:PickAndroidDevice}"
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to output debugging logs for diagnostics",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,25 +378,45 @@
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "android",
|
||||
"name": "Android",
|
||||
"request": "launch",
|
||||
"name": "Android launch",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
|
||||
"adbPort": 5037
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"request": "attach",
|
||||
"name": "Android attach",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"adbPort": 5037,
|
||||
"processId": "${command:PickAndroidProcess}"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Android: Launch Configuration",
|
||||
"label": "Android: Launch Application",
|
||||
"description": "A new configuration for launching an Android app debugging session",
|
||||
"body": {
|
||||
"type": "android",
|
||||
"request": "launch",
|
||||
"name": "${2:Launch App}",
|
||||
"name": "${2:Android Launch}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
|
||||
"adbPort": 5037
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Android: Attach to Process",
|
||||
"description": "A new configuration for attaching to a running Android app process",
|
||||
"body": {
|
||||
"type": "android",
|
||||
"request": "attach",
|
||||
"name": "${2:Android Attach}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"adbPort": 5037,
|
||||
"processId": "^\"\\${command:PickAndroidProcess}\""
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": {}
|
||||
@@ -131,24 +424,27 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "node ./node_modules/vscode/bin/install",
|
||||
"postinstall": "cd langserver && npm install && cd ..",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"long": "^4.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"vscode-debugadapter": "^1.32.0",
|
||||
"vscode-debugprotocol": "^1.32.0",
|
||||
"ws": "^7.1.2",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.27"
|
||||
"@vscode/debugadapter": "1.64.0",
|
||||
"@vscode/debugprotocol": "^1.64.0",
|
||||
"long": "^5.2.3",
|
||||
"unzipper": "0.10.14",
|
||||
"uuid": "9.0.1",
|
||||
"vscode-languageclient": "6.1.3",
|
||||
"ws": "8.16.0",
|
||||
"xmldom": "0.6.0",
|
||||
"xpath": "0.0.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.12.5",
|
||||
"eslint": "^5.9.0",
|
||||
"mocha": "^5.2.0",
|
||||
"typescript": "^3.1.6",
|
||||
"vscode": "^1.1.26"
|
||||
"@types/mocha": "^10.0.06",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/vscode": "^1.24.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsdoc": "^47.0.2",
|
||||
"mocha": "^10.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
1140
src/adbclient.js
1140
src/adbclient.js
File diff suppressed because it is too large
Load Diff
282
src/apk-decoder.js
Normal file
282
src/apk-decoder.js
Normal file
@@ -0,0 +1,282 @@
|
||||
const START_NAMESPACE_SPEC = {
|
||||
hdr: '0x0100',
|
||||
hdrsz: '0x0010',
|
||||
sz: '0x00000018',
|
||||
startline: 4,
|
||||
commentId: 4,
|
||||
nameId: 4,
|
||||
valueId: 4,
|
||||
}
|
||||
const END_NAMESPACE_SPEC = {
|
||||
hdr: '0x0101',
|
||||
hdrsz: '0x0010',
|
||||
sz: '0x00000018',
|
||||
startline: 4,
|
||||
commentId: 4,
|
||||
nameId: 4,
|
||||
valueId: 4,
|
||||
}
|
||||
const BEGIN_NODE_SPEC = {
|
||||
hdr: '0x0102',
|
||||
hdrsz: '0x0010',
|
||||
sz: 4,
|
||||
startline: 4,
|
||||
commentId: 4,
|
||||
namespaceId: 4,
|
||||
nameId: 4,
|
||||
attr: {
|
||||
offset: 2,
|
||||
size: 2,
|
||||
count: 2,
|
||||
},
|
||||
id_attr_offset: 2,
|
||||
cls_attr_offset: 2,
|
||||
style_attr_offset: 2,
|
||||
attributes: [{
|
||||
length: main => main.attr.count,
|
||||
element_spec: {
|
||||
ns: 4,
|
||||
nameId: 4,
|
||||
commentId: 4,
|
||||
sz: '0x0008',
|
||||
zero: '0x00',
|
||||
type: 1,
|
||||
value: 4,
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
const END_NODE_SPEC = {
|
||||
hdr: '0x0103',
|
||||
hdrsz: '0x0010',
|
||||
sz: 4,
|
||||
startline: 4,
|
||||
commentId: 4,
|
||||
namespaceId: 4,
|
||||
nameId: 4,
|
||||
}
|
||||
|
||||
function decode_spec_value(o, key, value, buf, idx, main) {
|
||||
let byteLength;
|
||||
switch (true) {
|
||||
case typeof value === 'number': {
|
||||
// raw integer value
|
||||
byteLength = value;
|
||||
o[key] = buf.readIntLE(idx, byteLength);
|
||||
break;
|
||||
}
|
||||
case Array.isArray(value): {
|
||||
// known-length array of values
|
||||
const length = value[0].length(main);
|
||||
byteLength = 0;
|
||||
o[key] = new Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const bytes = decode_spec_value(o[key], i, value[0].element_spec, buf, idx, main);
|
||||
idx += bytes;
|
||||
byteLength += bytes;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case typeof value === 'object': {
|
||||
// named sub-spec
|
||||
o[key] = {};
|
||||
byteLength = decode_spec(buf, value, o[key], o, idx);
|
||||
break
|
||||
}
|
||||
case /^0x[\da-fA-F]/.test(value): {
|
||||
// exact integer value
|
||||
byteLength = (value.length - 2) / 2;
|
||||
o[key] = buf.readUIntLE(idx, byteLength);
|
||||
if (parseInt(value) !== o[key]) {
|
||||
throw new Error(`Bad value. Expected ${value}, got 0x${o[key].toString(16)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case value === 'length-utf16-null': {
|
||||
// 2-byte length, utf16 chars, null char
|
||||
const string_byte_length = buf.readUInt16LE(idx) * 2; // 1 char = 2 bytes
|
||||
idx += 2;
|
||||
o[key] = buf.slice(idx, idx + string_byte_length).toString('ucs2');
|
||||
idx += string_byte_length;
|
||||
if (buf.readUInt16LE(idx) !== 0) {
|
||||
throw new Error(`Bad value. Nul char expected but not found.`);
|
||||
}
|
||||
byteLength = 2 + string_byte_length + 2;
|
||||
break;
|
||||
}
|
||||
case /^align:\d+$/.test(value): {
|
||||
// used for arbitrary padding to a specified alignment
|
||||
const align = parseInt(value.split(':')[1], 10);
|
||||
byteLength = idx - (Math.trunc(idx / align) * align);
|
||||
o[key] = buf.slice(idx, idx + byteLength);
|
||||
break;
|
||||
}
|
||||
default: throw new Error(`Unknown spec value definition: ${value}`);
|
||||
}
|
||||
return byteLength;
|
||||
}
|
||||
|
||||
function decode_spec(buf, spec, o = {}, main = o, idx = 0) {
|
||||
|
||||
let byteLength = 0;
|
||||
for (let key of Object.keys(spec)) {
|
||||
const value = spec[key];
|
||||
const bytes = decode_spec_value(o, key, value, buf, idx, main);
|
||||
idx += bytes;
|
||||
byteLength += bytes;
|
||||
}
|
||||
|
||||
return byteLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a binary XML file back into a readable XML document
|
||||
* @param {Buffer} buf binary XMl content
|
||||
*/
|
||||
function decode_binary_xml(buf) {
|
||||
const xml_spec = {
|
||||
header: '0x00080003',
|
||||
headerSize: 4,
|
||||
stringPool: {
|
||||
header: '0x0001',
|
||||
hdrsize: '0x001c',
|
||||
sz: 4,
|
||||
stringCount: 4,
|
||||
styleCount: 4,
|
||||
flags: 4,
|
||||
stringStart: 4,
|
||||
styleStart: 4,
|
||||
stringOffsets: [{
|
||||
length: main => main.stringPool.stringCount,
|
||||
element_spec: 4,
|
||||
}],
|
||||
strings: [{
|
||||
length: main => main.stringPool.stringCount,
|
||||
element_spec: 'length-utf16-null',
|
||||
}],
|
||||
padding: 'align:4',
|
||||
},
|
||||
resourceIDPool: {
|
||||
hdr: '0x0180',
|
||||
hdrsize: '0x0008',
|
||||
sz: 4,
|
||||
resIDs: [{
|
||||
length: main => (main.resourceIDPool.sz - main.resourceIDPool.hdrsize) / 4,
|
||||
element_spec: 4,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const decoded = {};
|
||||
let idx = decode_spec(buf, xml_spec, decoded);
|
||||
|
||||
// after we've extracted the string and id's, it should be time to parse the xml
|
||||
const node_stack = [{ nodes: [] }];
|
||||
const namespaces = [];
|
||||
while (idx < buf.byteLength) {
|
||||
const id = buf.readUInt16LE(idx);
|
||||
switch (id) {
|
||||
case 0x0100: {
|
||||
// start namespace
|
||||
const node = {};
|
||||
idx += decode_spec(buf, START_NAMESPACE_SPEC, node, node, idx);
|
||||
namespaces.push(node);
|
||||
break;
|
||||
}
|
||||
case 0x0101: {
|
||||
// end namespace
|
||||
const node = {};
|
||||
idx += decode_spec(buf, END_NAMESPACE_SPEC, node, node, idx);
|
||||
const i = namespaces.findIndex(ns => ns.nameId === node.nameId);
|
||||
namespaces.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
case 0x0102: {
|
||||
// begin node
|
||||
const node = {
|
||||
nodes: [],
|
||||
};
|
||||
idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx);
|
||||
node.namespaces = namespaces.slice();
|
||||
node.namespaces.forEach(ns => {
|
||||
if (!ns.node) ns.node = node;
|
||||
});
|
||||
node_stack[0].nodes.push(node);
|
||||
node_stack.unshift(node);
|
||||
break;
|
||||
}
|
||||
case 0x0103: {
|
||||
// end node
|
||||
const spec = END_NODE_SPEC;
|
||||
const node = {};
|
||||
idx += decode_spec(buf, spec, node, node, idx);
|
||||
node_stack.shift();
|
||||
break;
|
||||
}
|
||||
default: throw new Error(`Unknown XML element ${id.toString(16)}`);
|
||||
}
|
||||
}
|
||||
decoded.nodes = node_stack[0].nodes;
|
||||
|
||||
const xml = toXMLDocument(decoded);
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the decoded binary XML to a readable XML document
|
||||
* @param {*} decoded
|
||||
*/
|
||||
function toXMLDocument(decoded) {
|
||||
const strings = decoded.stringPool.strings;
|
||||
const format = {
|
||||
nodes: (nodes, indent) => {
|
||||
return nodes.map(node => format.node(node, indent)).join('\n');
|
||||
},
|
||||
node: (node, indent) => {
|
||||
const parts = [indent, '<', strings[node.nameId]];
|
||||
for (let ns of node.namespaces.filter(ns => ns.node === node)) {
|
||||
parts.push(' ', `xmlns:${strings[ns.nameId]}="${strings[ns.valueId]}"`);
|
||||
}
|
||||
const attr_indent = node.attributes.length > 1 ? `\n${indent} ` : ' ';
|
||||
for (let attr of node.attributes) {
|
||||
parts.push(attr_indent, format.attribute(attr, node.namespaces));
|
||||
}
|
||||
if (node.nodes.length) {
|
||||
parts.push('>\n', format.nodes(node.nodes, indent + ' '), '\n', indent, '</', strings[node.nameId], '>');
|
||||
} else {
|
||||
parts.push(' />');
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
},
|
||||
attribute: (attr, namespaces) => {
|
||||
let value = attr.value;
|
||||
switch (attr.type) {
|
||||
case 3:
|
||||
value = strings[value];
|
||||
break;
|
||||
case 16:
|
||||
value |= 0; // convert to signed integer
|
||||
break;
|
||||
case 18:
|
||||
value = value ? true : false;
|
||||
break;
|
||||
case 1: // resource id
|
||||
case 17: // flags
|
||||
default:
|
||||
value = '0x' + value.toString(`16`);
|
||||
break;
|
||||
}
|
||||
let ns = '';
|
||||
if (attr.ns >= 0) {
|
||||
ns = `${strings[namespaces.find(ns => ns.valueId === attr.ns).nameId]}:`;
|
||||
}
|
||||
return `${ns}${strings[attr.nameId]}="${value}"`;
|
||||
}
|
||||
}
|
||||
return '<?xml version="1.0" encoding="utf-8"?>\n' + format.nodes(decoded.nodes, '');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decode_binary_xml,
|
||||
}
|
||||
150
src/apk-file-info.js
Normal file
150
src/apk-file-info.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { extractManifestFromAPK, parseManifest } = require('./manifest');
|
||||
const { D } = require('./utils/print');
|
||||
|
||||
class APKFileInfo {
|
||||
/**
|
||||
* the full file path to the APK file
|
||||
*/
|
||||
fpn = '';
|
||||
|
||||
/**
|
||||
* The APK file data
|
||||
* @type {Buffer}
|
||||
*/
|
||||
file_data = null;
|
||||
|
||||
/**
|
||||
* The size of the APK file (in bytes)
|
||||
*/
|
||||
file_size = 0;
|
||||
|
||||
/**
|
||||
* last modified time of the APK file (in ms)
|
||||
*/
|
||||
app_modified = 0;
|
||||
|
||||
/**
|
||||
* SHA-1 (hex) digest of the APK file
|
||||
*/
|
||||
content_hash = '';
|
||||
|
||||
/**
|
||||
* Contents of Android Manifest XML file
|
||||
*/
|
||||
manifestXml = '';
|
||||
|
||||
/**
|
||||
* Extracted data from the manifest
|
||||
*/
|
||||
manifest = {
|
||||
/**
|
||||
* Package name of the app
|
||||
*/
|
||||
package: '',
|
||||
|
||||
/**
|
||||
* List of all named Activities
|
||||
* @type {string[]}
|
||||
*/
|
||||
activities: [],
|
||||
|
||||
/**
|
||||
* The launcher Activity
|
||||
*/
|
||||
launcher: '',
|
||||
};
|
||||
|
||||
constructor(apk_fpn) {
|
||||
this.fpn = apk_fpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new APKFileInfo instance
|
||||
* @param {*} args
|
||||
*/
|
||||
static async from(args) {
|
||||
const result = new APKFileInfo(args.apkFile);
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
// save the last modification time of the app
|
||||
result.app_modified = fs.statSync(result.fpn).mtime.getTime();
|
||||
|
||||
// create a SHA-1 hash as a simple way to see if we need to install/update the app
|
||||
const h = crypto.createHash('SHA1');
|
||||
h.update(result.file_data);
|
||||
result.content_hash = h.digest('hex');
|
||||
|
||||
// read the manifest
|
||||
try {
|
||||
result.manifestXml = await getAndroidManifestXml(args);
|
||||
} catch (err) {
|
||||
throw new Error(`Manifest read error. ${err.message}`);
|
||||
}
|
||||
// extract the parts we need from the manifest
|
||||
try {
|
||||
result.manifest = parseManifest(result.manifestXml);
|
||||
} catch(err) {
|
||||
throw new Error(`Manifest parse failed. ${err.message}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the AndroidManifest.xml file content
|
||||
*
|
||||
* Because of manifest merging and build-injected properties, the manifest compiled inside
|
||||
* the APK is frequently different from the AndroidManifest.xml source file.
|
||||
* We try to extract the manifest from 3 sources (in priority order):
|
||||
* 1. The 'manifestFile' launch configuration property
|
||||
* 2. The decoded manifest from the APK
|
||||
* 3. The AndroidManifest.xml file from the root of the source tree.
|
||||
*/
|
||||
async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) {
|
||||
let manifest;
|
||||
|
||||
// a value from the manifestFile overrides the default manifest extraction
|
||||
// note: there's no validation that the file is a valid AndroidManifest.xml file
|
||||
if (manifestFile) {
|
||||
D(`Reading manifest from ${manifestFile}`);
|
||||
manifest = await readFile(manifestFile, 'utf8');
|
||||
return manifest;
|
||||
}
|
||||
|
||||
try {
|
||||
D(`Reading APK Manifest`);
|
||||
manifest = await extractManifestFromAPK(apkFile);
|
||||
} catch(err) {
|
||||
// if we fail to get manifest from the APK, revert to the source file version
|
||||
D(`Reading source manifest from ${appSrcRoot} (${err.message})`);
|
||||
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisified fs.readFile()
|
||||
* @param {string} path
|
||||
* @param {*} [options]
|
||||
*/
|
||||
function readFile(path, options) {
|
||||
return new Promise((res, rej) => {
|
||||
fs.readFile(path, options || {}, (err, data) => {
|
||||
err ? rej(err) : res(data);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APKFileInfo,
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
const net = require('net');
|
||||
const D = require('./util').D;
|
||||
|
||||
var sockets_by_id = {};
|
||||
var last_socket_id = 0;
|
||||
|
||||
const chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
q:{},
|
||||
get(o, cb) {
|
||||
for (var key in o) {
|
||||
var x = this.q[key];
|
||||
if (typeof(x) !== 'undefined') o[key] = x;
|
||||
}
|
||||
process.nextTick(cb, o);
|
||||
},
|
||||
set(obj, cb) {
|
||||
for (var key in obj)
|
||||
this.q[key] = obj[key];
|
||||
process.nextTick(cb);
|
||||
}
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
lastError:null,
|
||||
_noError() { this.lastError = null }
|
||||
},
|
||||
permissions: {
|
||||
request(usbPermissions, cb) {
|
||||
process.nextTick(cb, true);
|
||||
}
|
||||
},
|
||||
socket: {
|
||||
listen(socketId, host, port, max_connections, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.listen(port, host, max_connections);
|
||||
process.nextTick(cb => {
|
||||
chrome.runtime._noError();
|
||||
cb(0);
|
||||
}, cb);
|
||||
},
|
||||
connect(socketId, host, port, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.connect({port:port,host:host}, function(){
|
||||
chrome.runtime._noError();
|
||||
this.s.onerror = null;
|
||||
this.cb.call(null,0);
|
||||
}.bind({s:s,cb:cb}));
|
||||
s.onerror = function(e) {
|
||||
this.s.onerror = null;
|
||||
this.cb.call(null,-1);
|
||||
}.bind({s:s,cb:cb});
|
||||
},
|
||||
disconnect(socketId) {
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.end();
|
||||
},
|
||||
setNoDelay(socketId, state, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.setNoDelay(state);
|
||||
process.nextTick(cb => {
|
||||
chrome.runtime._noError();
|
||||
cb(1);
|
||||
}, cb);
|
||||
},
|
||||
read(socketId, bufferSize, onRead) {
|
||||
if (!onRead && typeof(bufferSize) === 'function')
|
||||
onRead = bufferSize, bufferSize=-1;
|
||||
if (!onRead) return;
|
||||
var s = sockets_by_id[socketId];
|
||||
if (bufferSize === 0) {
|
||||
process.nextTick(function(onRead) {
|
||||
chrome.runtime._noError();
|
||||
onRead.call(null, {resultCode:1,data:Buffer.alloc(0)});
|
||||
}, onRead);
|
||||
return;
|
||||
}
|
||||
s.read_requests.push({onRead:onRead, bufferSize:bufferSize});
|
||||
if (s.read_requests.length > 1) {
|
||||
return;
|
||||
}
|
||||
!s.ondata && s._raw.on('data', s.ondata = function(data) {
|
||||
this.readbuffer = Buffer.concat([this.readbuffer, data]);
|
||||
while(this.read_requests.length) {
|
||||
var amount = this.read_requests[0].bufferSize;
|
||||
if (amount <= 0) amount = this.readbuffer.length;
|
||||
if (amount > this.readbuffer.length || this.readbuffer.length === 0)
|
||||
return; // wait for more data
|
||||
var readInfo = {
|
||||
resultCode:1,
|
||||
data:Buffer.from(this.readbuffer.slice(0,amount)),
|
||||
};
|
||||
this.readbuffer = this.readbuffer.slice(amount);
|
||||
chrome.runtime._noError();
|
||||
this.read_requests.shift().onRead.call(null,readInfo);
|
||||
}
|
||||
this.onerror = this.onclose = null;
|
||||
}.bind(s));
|
||||
var on_read_terminated = function(e) {
|
||||
this.readbuffer = Buffer.alloc(0);
|
||||
while(this.read_requests.length) {
|
||||
var readInfo = {
|
||||
resultCode:-1, // <=0 for error
|
||||
};
|
||||
this.read_requests.shift().onRead.call(null,readInfo);
|
||||
}
|
||||
this.onerror = this.onclose = null;
|
||||
}.bind(s);
|
||||
!s.onerror && (s.onerror = on_read_terminated);
|
||||
!s.onclose && (s.onclose = on_read_terminated);
|
||||
if (s.readbuffer.length || bufferSize < 0) {
|
||||
process.nextTick(s.ondata, Buffer.alloc(0));
|
||||
}
|
||||
},
|
||||
write(socketId, data, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
if (!(data instanceof Buffer))
|
||||
data = Buffer.from(data);
|
||||
s._raw.write(data, function(e,f,g) {
|
||||
if (this.s.write_cbs.length === 1)
|
||||
this.s.onerror = null;
|
||||
var writeInfo = {
|
||||
bytesWritten: this.len,
|
||||
};
|
||||
chrome.runtime._noError();
|
||||
this.s.write_cbs.shift().call(null, writeInfo);
|
||||
}.bind({s:s,len:data.length,cb:cb}));
|
||||
s.write_cbs.push(cb);
|
||||
if (!s.onerror) {
|
||||
s.onerror = function(e) {
|
||||
this.s.onerror = null;
|
||||
while (this.s.write_cbs.length) {
|
||||
var writeInfo = {
|
||||
bytesWritten: 0,
|
||||
};
|
||||
this.s.write_cbs.shift().call(null, writeInfo);
|
||||
}
|
||||
}.bind({s:s});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
create_socket:function(id, type, cb) {
|
||||
if (!cb && typeof(type) === 'function') {
|
||||
cb = type, type = null;
|
||||
}
|
||||
var socket = type === 'server' ? new net.Server() : new net.Socket();
|
||||
var socketInfo = {
|
||||
id: id,
|
||||
socketId: ++last_socket_id,
|
||||
_raw: socket,
|
||||
onerror:null,
|
||||
onclose:null,
|
||||
write_cbs:[],
|
||||
read_requests:[],
|
||||
readbuffer:Buffer.alloc(0),
|
||||
};
|
||||
socketInfo._raw.on('error', function(e) {
|
||||
chrome.runtime.lastError = e;
|
||||
this.onerror && this.onerror(e);
|
||||
}.bind(socketInfo));
|
||||
socketInfo._raw.on('close', function(e) {
|
||||
this.onclose && this.onclose(e);
|
||||
}.bind(socketInfo));
|
||||
sockets_by_id[socketInfo.socketId] = socketInfo;
|
||||
process.nextTick(cb, socketInfo);
|
||||
},
|
||||
create_chrome_socket(id, type, cb) { return chrome.create_socket(id, type, cb) },
|
||||
|
||||
accept_socket:function(id, socketId, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
if (s.onconnection) {
|
||||
s.onconnection = cb;
|
||||
} else {
|
||||
s.onconnection = cb;
|
||||
s._raw.on('connection', function(client_socket) {
|
||||
var acceptInfo = {
|
||||
socketId: ++last_socket_id,
|
||||
_raw: client_socket,
|
||||
}
|
||||
sockets_by_id[acceptInfo.socketId] = acceptInfo;
|
||||
this.onconnection(acceptInfo);
|
||||
}.bind(s));
|
||||
}
|
||||
},
|
||||
accept_chrome_socket(id, socketId, cb) { return chrome.accept_socket(id, socketId, cb) },
|
||||
|
||||
destroy_socket:function(socketId) {
|
||||
var s = sockets_by_id[socketId];
|
||||
if (!s) return;
|
||||
s._raw.end();
|
||||
sockets_by_id[socketId] = null;
|
||||
},
|
||||
destroy_chrome_socket(socketId) { return chrome.destroy_socket(socketId) },
|
||||
}
|
||||
|
||||
exports.chrome = chrome;
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict'
|
||||
// vscode stuff
|
||||
const { workspace, EventEmitter, Uri } = require('vscode');
|
||||
const vscode = require('vscode');
|
||||
const { workspace, EventEmitter, Uri } = vscode;
|
||||
|
||||
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
||||
class AndroidContentProvider {
|
||||
|
||||
constructor() {
|
||||
this._docs = {}; // hashmap<url, LogcatContent>
|
||||
/** @type {Map<vscode.Uri,*>} */
|
||||
this._docs = new Map(); // Map<uri, LogcatContent>
|
||||
this._onDidChange = new EventEmitter();
|
||||
}
|
||||
|
||||
@@ -27,13 +27,15 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
||||
* [document](TextDocument). Resources allocated should be released when
|
||||
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
|
||||
*
|
||||
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
||||
* @param token A cancellation token.
|
||||
* @return A string or a thenable that resolves to such.
|
||||
* @param {vscode.Uri} uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
||||
* @param {vscode.CancellationToken} _token A cancellation token.
|
||||
* @return {string|Thenable<string>} A string or a thenable that resolves to such.
|
||||
*/
|
||||
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
|
||||
var doc = this._docs[uri];
|
||||
if (doc) return this._docs[uri].content;
|
||||
provideTextDocumentContent(uri, _token) {
|
||||
const doc = this._docs.get(uri);
|
||||
if (doc) {
|
||||
return doc.content();
|
||||
}
|
||||
switch (uri.authority) {
|
||||
// android-dev-ext://logcat/read?<deviceid>
|
||||
case 'logcat': return this.provideLogcatDocumentContent(uri);
|
||||
@@ -41,38 +43,51 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
||||
throw new Error('Document Uri not recognised');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {vscode.Uri} uri
|
||||
*/
|
||||
provideLogcatDocumentContent(uri) {
|
||||
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
|
||||
const { LogcatContent } = require('./logcat');
|
||||
var doc = this._docs[uri] = new LogcatContent(uri.query);
|
||||
return doc.content;
|
||||
const doc = new LogcatContent(uri.query);
|
||||
this._docs.set(uri, doc);
|
||||
return doc.content();
|
||||
}
|
||||
}
|
||||
|
||||
// the statics
|
||||
AndroidContentProvider.SCHEME = 'android-dev-ext';
|
||||
|
||||
AndroidContentProvider.register = (ctx, workspace) => {
|
||||
var provider = new AndroidContentProvider();
|
||||
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
||||
ctx.subscriptions.push(registration);
|
||||
ctx.subscriptions.push(provider);
|
||||
const provider = new AndroidContentProvider();
|
||||
const registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
|
||||
ctx.subscriptions.push(registration, provider);
|
||||
}
|
||||
|
||||
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
|
||||
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
|
||||
const uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
|
||||
return uri.with({
|
||||
query: deviceId
|
||||
});
|
||||
}
|
||||
|
||||
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
|
||||
// there's surely got to be a better way than this...
|
||||
var configs = workspace.getConfiguration('launch.configurations');
|
||||
for (var i=0,config; config=configs.get(''+i); i++) {
|
||||
if (config.type!=='android') continue;
|
||||
if (config.request!=='launch') continue;
|
||||
if (config[name]) return config[name];
|
||||
const configs = workspace.getConfiguration('launch.configurations');
|
||||
for (let i = 0, config; config = configs.get(`${i}`); i++) {
|
||||
if (config.type!=='android') {
|
||||
continue;
|
||||
}
|
||||
if (config.request!=='launch') {
|
||||
continue;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(config, name)) {
|
||||
return config[name];
|
||||
}
|
||||
break;
|
||||
}
|
||||
return defvalue;
|
||||
}
|
||||
|
||||
exports.AndroidContentProvider = AndroidContentProvider;
|
||||
module.exports = {
|
||||
AndroidContentProvider,
|
||||
}
|
||||
|
||||
2062
src/debugMain.js
2062
src/debugMain.js
File diff suppressed because it is too large
Load Diff
794
src/debugger-types.js
Normal file
794
src/debugger-types.js
Normal file
@@ -0,0 +1,794 @@
|
||||
const { splitSourcePath } = require('./utils/source-file');
|
||||
|
||||
/**
|
||||
* @typedef {import('./package-searcher').PackageInfo} PackageInfo
|
||||
* @typedef {import('./adbclient').ADBClient} ADBClient
|
||||
*/
|
||||
|
||||
class BuildInfo {
|
||||
/**
|
||||
* @param {Map<string,PackageInfo>} packages
|
||||
*/
|
||||
constructor(packages) {
|
||||
this.packages = packages;
|
||||
}
|
||||
}
|
||||
|
||||
class LaunchBuildInfo extends BuildInfo {
|
||||
/**
|
||||
* @param {Map<string,PackageInfo>} packages
|
||||
* @param {string} pkgname
|
||||
* @param {string} launchActivity
|
||||
* @param {string[]} amCommandArgs custom arguments passed to `am start`
|
||||
* @param {number} postLaunchPause amount of time (in ms) to wait after launch before we attempt a debugger connection
|
||||
*/
|
||||
constructor(packages, pkgname, launchActivity, amCommandArgs, postLaunchPause) {
|
||||
super(packages);
|
||||
this.pkgname = pkgname;
|
||||
this.launchActivity = launchActivity;
|
||||
/** the arguments passed to `am start` */
|
||||
this.startCommandArgs = amCommandArgs || [
|
||||
'-D', // enable debugging
|
||||
'--activity-brought-to-front',
|
||||
'-a android.intent.action.MAIN',
|
||||
'-c android.intent.category.LAUNCHER',
|
||||
`-n ${pkgname}/${launchActivity}`,
|
||||
];
|
||||
/**
|
||||
* the amount of time (in millis) to wait after 'am start ...' is invoked.
|
||||
* We need this because invoking JDWP too soon causes a hang.
|
||||
*/
|
||||
this.postLaunchPause = ((typeof postLaunchPause === 'number') && (postLaunchPause >= 0)) ? postLaunchPause : 1000;
|
||||
}
|
||||
}
|
||||
|
||||
class AttachBuildInfo extends BuildInfo {
|
||||
/**
|
||||
* @param {Map<string,PackageInfo>} packages
|
||||
*/
|
||||
constructor(packages) {
|
||||
super(packages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single debugger session
|
||||
*/
|
||||
class DebugSession {
|
||||
|
||||
/**
|
||||
* @param {BuildInfo} build
|
||||
* @param {string} deviceid
|
||||
*/
|
||||
constructor(build, deviceid) {
|
||||
/**
|
||||
* Build information for this session
|
||||
*/
|
||||
this.build = build;
|
||||
|
||||
/**
|
||||
* The device ID of the device being debugged
|
||||
*/
|
||||
this.deviceid = deviceid;
|
||||
|
||||
/**
|
||||
* The ADB connection to the device being debugged
|
||||
* @type {ADBClient}
|
||||
*/
|
||||
this.adbclient = null;
|
||||
|
||||
/**
|
||||
* Location of the last stop event (breakpoint, exception, step)
|
||||
* @type {SourceLocation}
|
||||
*/
|
||||
this.stoppedLocation = null;
|
||||
|
||||
/**
|
||||
* The entire list of retrieved types during the debug session
|
||||
* @type {DebuggerTypeInfo[]}
|
||||
*/
|
||||
this.classList = [];
|
||||
|
||||
/**
|
||||
* Map of type signatures to cached types
|
||||
* @type {Map<string,DebuggerTypeInfo | Promise<DebuggerTypeInfo>>}
|
||||
*/
|
||||
this.classCache = new Map();
|
||||
|
||||
/**
|
||||
* The class-prepare filters set up on the device
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
this.classPrepareFilters = new Set();
|
||||
|
||||
/**
|
||||
* The set of class signatures loaded by the runtime
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
this.loadedClasses = new Set();
|
||||
|
||||
/**
|
||||
* Enabled step JDWP IDs for each thread
|
||||
* @type {Map<JavaThreadID, StepID>}
|
||||
*/
|
||||
this.stepIDs = new Map();
|
||||
|
||||
/**
|
||||
* The counts of thread-suspend calls. A thread is only resumed when the
|
||||
* all suspend calls are matched with resume calls.
|
||||
* @type {Map<JavaThreadID, number>}
|
||||
*/
|
||||
this.threadSuspends = new Map();
|
||||
|
||||
/**
|
||||
* The queue of pending method invoke expressions to be called for each thread.
|
||||
* Method invokes can only be called sequentially on a per-thread basis.
|
||||
* @type {Map<JavaThreadID, *[]>}
|
||||
*/
|
||||
this.methodInvokeQueues = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
class JavaTaggedValue {
|
||||
/**
|
||||
*
|
||||
* @param {string|number|boolean} value
|
||||
* @param {JavaValueType} valuetype
|
||||
*/
|
||||
constructor(value, valuetype) {
|
||||
this.value = value;
|
||||
this.valuetype = valuetype;
|
||||
}
|
||||
|
||||
static signatureToJavaValueType(s) {
|
||||
return {
|
||||
B: 'byte',C:'char',D:'double',F:'float',I:'int',J:'long','S':'short',V:'void',Z:'boolean'
|
||||
}[s[0]] || 'oref';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DebuggerValue} v
|
||||
* @param {string} [signature]
|
||||
*/
|
||||
static from(v, signature) {
|
||||
return new JavaTaggedValue(v.value, JavaTaggedValue.signatureToJavaValueType(signature || v.type.signature));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class of Java types
|
||||
*/
|
||||
class JavaType {
|
||||
|
||||
/**
|
||||
* @param {string} signature JRE type signature
|
||||
* @param {string} typename human-readable type name
|
||||
* @param {boolean} [invalid] true if the type could not be parsed from the signature
|
||||
*/
|
||||
constructor(signature, typename, invalid) {
|
||||
this.signature = signature;
|
||||
this.typename = typename;
|
||||
if (invalid) {
|
||||
this.invalid = invalid;
|
||||
}
|
||||
}
|
||||
|
||||
fullyQualifiedName() {
|
||||
return this.typename;
|
||||
}
|
||||
|
||||
/** @type {Map<string, JavaType>} */
|
||||
static _cache = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} signature
|
||||
* @returns {JavaType}
|
||||
*/
|
||||
static from(signature) {
|
||||
let type = JavaType._cache.get(signature);
|
||||
if (!type) {
|
||||
type = JavaClassType.from(signature)
|
||||
|| JavaArrayType.from(signature)
|
||||
|| JavaPrimitiveType.from(signature)
|
||||
|| new JavaType(signature, signature, true);
|
||||
JavaType._cache.set(signature, type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
static get Object() {
|
||||
return JavaType.from('Ljava/lang/Object;');
|
||||
}
|
||||
|
||||
static get String() {
|
||||
return JavaType.from('Ljava/lang/String;');
|
||||
}
|
||||
|
||||
static get byte() {
|
||||
return JavaType.from('B');
|
||||
}
|
||||
static get short() {
|
||||
return JavaType.from('S');
|
||||
}
|
||||
static get int() {
|
||||
return JavaType.from('I');
|
||||
}
|
||||
static get long() {
|
||||
return JavaType.from('J');
|
||||
}
|
||||
static get float() {
|
||||
return JavaType.from('F');
|
||||
}
|
||||
static get double() {
|
||||
return JavaType.from('D');
|
||||
}
|
||||
static get char() {
|
||||
return JavaType.from('C');
|
||||
}
|
||||
static get boolean() {
|
||||
return JavaType.from('Z');
|
||||
}
|
||||
static null = new JavaType('Lnull;', 'null'); // null has no type really, but we need something for literals
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isArray(t) { return /^\[/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isByte(t) { return /^B$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isClass(t) { return /^L/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isReference(t) { return /^[L[]/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isPrimitive(t) { return /^[BCIJSFDZ]$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isInteger(t) { return /^[BIS]$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isLong(t) { return /^J$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isFloat(t) { return /^[FD]$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isArrayIndex(t) { return /^[BCIJS]$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isString(t) { return t.signature === this.String.signature }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isChar(t) { return t.signature === this.char.signature }
|
||||
|
||||
/**
|
||||
* @param {JavaType} t
|
||||
*/
|
||||
static isBoolean(t) { return t.signature === this.boolean.signature }
|
||||
}
|
||||
|
||||
class JavaClassType extends JavaType {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} signature
|
||||
* @param {string} package_name
|
||||
* @param {string} typename
|
||||
* @param {boolean} anonymous
|
||||
*/
|
||||
constructor(signature, package_name, typename, anonymous) {
|
||||
super(signature, typename);
|
||||
this.package = package_name;
|
||||
this.anonymous = anonymous;
|
||||
}
|
||||
|
||||
fullyQualifiedName() {
|
||||
return this.package ? `${this.package}.${this.typename}` : this.typename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} signature
|
||||
*/
|
||||
static from(signature) {
|
||||
const class_match = signature.match(/^L([^$]+)\/([^$\/]+)(\$.+)?;$/);
|
||||
if (!class_match) {
|
||||
return null;
|
||||
}
|
||||
const package_name = class_match[1].replace(/\//g,'.');
|
||||
const typename = (class_match[2]+(class_match[3]||'')).replace(/\$(?=[^\d])/g,'.');
|
||||
const anonymous = /\$\d/.test(class_match[3]);
|
||||
return new JavaClassType(signature, package_name, typename, anonymous);
|
||||
}
|
||||
}
|
||||
|
||||
class JavaArrayType extends JavaType {
|
||||
|
||||
/**
|
||||
* @param {string} signature JRE type signature
|
||||
* @param {number} arraydims number of array dimensions
|
||||
* @param {JavaType} elementType array element type
|
||||
*/
|
||||
constructor(signature, arraydims, elementType) {
|
||||
super(signature, `${elementType.typename}[]`);
|
||||
this.arraydims = arraydims;
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
fullyQualifiedName() {
|
||||
return `${this.elementType.fullyQualifiedName()}[]`;
|
||||
}
|
||||
|
||||
static from(signature) {
|
||||
const array_match = signature.match(/^(\[+)(.+)$/);
|
||||
if (!array_match) {
|
||||
return null;
|
||||
}
|
||||
const elementType = JavaType.from(array_match[1].slice(0,-1) + array_match[2]);
|
||||
return new JavaArrayType(signature, array_match[1].length, elementType);
|
||||
}
|
||||
}
|
||||
|
||||
class JavaPrimitiveType extends JavaType {
|
||||
|
||||
/**
|
||||
* @param {string} signature
|
||||
* @param {string} typename
|
||||
*/
|
||||
constructor(signature, typename) {
|
||||
super(signature, typename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} signature
|
||||
*/
|
||||
static from(signature) {
|
||||
return Object.prototype.hasOwnProperty.call(JavaPrimitiveType.bySignature, signature)
|
||||
? JavaPrimitiveType.bySignature[signature]
|
||||
: null;
|
||||
}
|
||||
|
||||
static bySignature = {
|
||||
B: new JavaPrimitiveType('B', 'byte'),
|
||||
C: new JavaPrimitiveType('C', 'char'),
|
||||
F: new JavaPrimitiveType('F', 'float'),
|
||||
D: new JavaPrimitiveType('D', 'double'),
|
||||
I: new JavaPrimitiveType('I', 'int'),
|
||||
J: new JavaPrimitiveType('J', 'long'),
|
||||
S: new JavaPrimitiveType('S', 'short'),
|
||||
V: new JavaPrimitiveType('V', 'void'),
|
||||
Z: new JavaPrimitiveType('Z', 'boolean'),
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggerValue {
|
||||
|
||||
/**
|
||||
* @param {DebuggerValueType} vtype
|
||||
* @param {JavaType} type
|
||||
* @param {*} value
|
||||
* @param {boolean} valid
|
||||
* @param {boolean} hasnullvalue
|
||||
* @param {string} name
|
||||
* @param {*} data
|
||||
*/
|
||||
constructor(vtype, type, value, valid, hasnullvalue, name, data) {
|
||||
this.vtype = vtype;
|
||||
this.hasnullvalue = hasnullvalue;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.valid = valid;
|
||||
this.value = value;
|
||||
this.data = data;
|
||||
|
||||
/** @type {string} */
|
||||
this.string = null;
|
||||
/** @type {number} */
|
||||
this.biglen = null;
|
||||
/** @type {number} */
|
||||
this.arraylen = null;
|
||||
/** @type {string} */
|
||||
this.fqname = null;
|
||||
}
|
||||
}
|
||||
|
||||
class LiteralValue extends DebuggerValue {
|
||||
/**
|
||||
* @param {JavaType} type
|
||||
* @param {*} value
|
||||
* @param {boolean} [hasnullvalue]
|
||||
* @param {*} [data]
|
||||
*/
|
||||
constructor(type, value, hasnullvalue = false, data = null) {
|
||||
super('literal', type, value, true, hasnullvalue, '', data);
|
||||
}
|
||||
|
||||
static Null = new LiteralValue(JavaType.null, '0000000000000000', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class of all debugger events invoked by JDWP
|
||||
*/
|
||||
class DebuggerEvent {
|
||||
constructor(event) {
|
||||
this.event = event;
|
||||
}
|
||||
}
|
||||
|
||||
class JavaBreakpointEvent extends DebuggerEvent {
|
||||
/**
|
||||
*
|
||||
* @param {*} event
|
||||
* @param {SourceLocation} stoppedLocation
|
||||
* @param {DebuggerBreakpoint} breakpoint
|
||||
*/
|
||||
constructor(event, stoppedLocation, breakpoint) {
|
||||
super(event)
|
||||
this.stoppedLocation = stoppedLocation;
|
||||
this.bp = breakpoint;
|
||||
}
|
||||
}
|
||||
|
||||
class JavaExceptionEvent extends DebuggerEvent {
|
||||
/**
|
||||
* @param {JavaObjectID} event
|
||||
* @param {SourceLocation} throwlocation
|
||||
* @param {SourceLocation} catchlocation
|
||||
*/
|
||||
constructor(event, throwlocation, catchlocation) {
|
||||
super(event);
|
||||
this.throwlocation = throwlocation;
|
||||
this.catchlocation = catchlocation;
|
||||
};
|
||||
}
|
||||
|
||||
class DebuggerException {
|
||||
/**
|
||||
* @param {DebuggerValue} exceptionValue
|
||||
* @param {JavaThreadID} threadid
|
||||
*/
|
||||
constructor(exceptionValue, threadid) {
|
||||
this.exceptionValue = exceptionValue;
|
||||
this.threadid = threadid;
|
||||
/** @type {VSCVariableReference} */
|
||||
this.scopeRef = null;
|
||||
/** @type {VSCVariableReference} */
|
||||
this.frameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakpointLocation {
|
||||
/**
|
||||
* @param {DebuggerBreakpoint} bp
|
||||
* @param {DebuggerTypeInfo} c
|
||||
* @param {DebuggerMethodInfo} m
|
||||
* @param {hex64} l
|
||||
*/
|
||||
constructor(bp, c, m, l) {
|
||||
this.bp = bp;
|
||||
this.c = c;
|
||||
this.m = m;
|
||||
this.l = l;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceLocation {
|
||||
|
||||
/**
|
||||
* @param {string} qtype
|
||||
* @param {number} linenum
|
||||
* @param {boolean} exact
|
||||
* @param {JavaThreadID} threadid
|
||||
*/
|
||||
constructor(qtype, linenum, exact, threadid) {
|
||||
this.qtype = qtype;
|
||||
this.linenum = linenum;
|
||||
this.exact = exact;
|
||||
this.threadid = threadid;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggerMethodInfo {
|
||||
|
||||
/**
|
||||
* @param {JavaMethod} m
|
||||
* @param {DebuggerTypeInfo} owningclass
|
||||
*/
|
||||
constructor(m, owningclass) {
|
||||
this._method = m;
|
||||
this.owningclass = owningclass;
|
||||
/** @type {JavaVarTable} */
|
||||
this.vartable = null;
|
||||
/** @type {JavaLineTable} */
|
||||
this.linetable = null;
|
||||
}
|
||||
|
||||
get genericsig() { return this._method.genericsig }
|
||||
|
||||
get methodid() { return this._method.methodid }
|
||||
|
||||
/**
|
||||
* https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1
|
||||
*/
|
||||
get modbits() { return this._method.modbits }
|
||||
|
||||
get name() { return this._method.name }
|
||||
|
||||
get sig() { return this._method.sig }
|
||||
|
||||
get isStatic() {
|
||||
return (this._method.modbits & 0x0008) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaLineTable} linetable
|
||||
*/
|
||||
setLineTable(linetable) {
|
||||
return this.linetable = linetable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JavaVarTable} vartable
|
||||
*/
|
||||
setVarTable(vartable) {
|
||||
return this.vartable = vartable;
|
||||
}
|
||||
|
||||
get returnTypeSignature() {
|
||||
return (this._method.genericsig || this._method.sig).match(/\)(.+)$/)[1];
|
||||
}
|
||||
|
||||
static NullLineTable = {
|
||||
start: '0000000000000000',
|
||||
end: '0000000000000000',
|
||||
lines: [],
|
||||
};
|
||||
}
|
||||
|
||||
class DebuggerFrameInfo {
|
||||
/**
|
||||
*
|
||||
* @param {JavaFrame} frame
|
||||
* @param {DebuggerMethodInfo} method
|
||||
* @param {JavaThreadID} threadid
|
||||
*/
|
||||
constructor(frame, method, threadid) {
|
||||
this._frame = frame;
|
||||
this.method = method;
|
||||
this.threadid = threadid;
|
||||
}
|
||||
|
||||
get frameid() {
|
||||
return this._frame.frameid;
|
||||
}
|
||||
|
||||
get location() {
|
||||
return this._frame.location;
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggerBreakpoint {
|
||||
|
||||
/**
|
||||
* @param {string} srcfpn
|
||||
* @param {number} linenum
|
||||
* @param {BreakpointOptions} options
|
||||
* @param {BreakpointState} initialState
|
||||
*/
|
||||
constructor(srcfpn, linenum, options, initialState = 'set') {
|
||||
const cls = splitSourcePath(srcfpn);
|
||||
this.id = DebuggerBreakpoint.makeBreakpointID(srcfpn, linenum);
|
||||
this.srcfpn = srcfpn;
|
||||
this.qtype = cls.qtype;
|
||||
this.pkg = cls.pkg;
|
||||
this.type = cls.type;
|
||||
this.linenum = linenum;
|
||||
this.options = options;
|
||||
this.sigpattern = new RegExp(`^L${cls.qtype}([$][$a-zA-Z0-9_]+)?;$`),
|
||||
this.state = initialState; // set,notloaded,enabled,removed
|
||||
this.hitcount = 0; // number of times this bp was hit during execution
|
||||
this.stopcount = 0; // number of times this bp caused a break into the debugger
|
||||
this.vsbp = null;
|
||||
this.enabled = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BreakpointLocation} bploc
|
||||
* @param {number} requestid JDWP request ID for the breakpoint
|
||||
*/
|
||||
setEnabled(bploc, requestid) {
|
||||
this.enabled = {
|
||||
/** @type {CMLKey} */
|
||||
cml: `${bploc.c.info.typeid}:${bploc.m.methodid}:${bploc.l}`,
|
||||
bp: this,
|
||||
bploc: {
|
||||
c: bploc.c,
|
||||
m: bploc.m,
|
||||
l: bploc.l,
|
||||
},
|
||||
requestid,
|
||||
}
|
||||
}
|
||||
|
||||
setDisabled() {
|
||||
this.enabled = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a unique breakpoint ID from the source path and line number
|
||||
* @param {string} srcfpn
|
||||
* @param {number} line
|
||||
* @returns {BreakpointID}
|
||||
*/
|
||||
static makeBreakpointID(srcfpn, line) {
|
||||
const cls = splitSourcePath(srcfpn);
|
||||
return `${line}:${cls.qtype}`;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakpointOptions {
|
||||
/**
|
||||
* Hit-count used for conditional breakpoints
|
||||
* @type {number|null}
|
||||
*/
|
||||
hitcount = null;
|
||||
}
|
||||
|
||||
class DebuggerTypeInfo {
|
||||
|
||||
/**
|
||||
* @param {JavaClassInfo} info
|
||||
* @param {JavaType} type
|
||||
*/
|
||||
constructor(info, type) {
|
||||
this.info = info;
|
||||
this.type = type;
|
||||
|
||||
/** @type {JavaField[]} */
|
||||
this.fields = null;
|
||||
|
||||
/** @type {DebuggerMethodInfo[]} */
|
||||
this.methods = null;
|
||||
|
||||
/** @type {JavaSource} */
|
||||
this.src = null;
|
||||
|
||||
// if it's not a class type, set super to null
|
||||
// otherwise, leave super undefined to be updated later
|
||||
if (info.reftype.string !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) {
|
||||
if (info.reftype.string !== 'array') {
|
||||
/** @type {JavaClassType} */
|
||||
this.super = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.type.typename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy type info for when the Java runtime hasn't loaded the class.
|
||||
*/
|
||||
class TypeNotAvailable extends DebuggerTypeInfo {
|
||||
/** @type {JavaClassInfo} */
|
||||
static info = {
|
||||
reftype: 0,
|
||||
status: null,
|
||||
type: null,
|
||||
typeid: '',
|
||||
}
|
||||
|
||||
constructor(type) {
|
||||
super(TypeNotAvailable.info, type);
|
||||
this.fields = [];
|
||||
this.methods = [];
|
||||
}
|
||||
}
|
||||
|
||||
class JavaThreadInfo {
|
||||
/**
|
||||
* @param {JavaThreadID} threadid
|
||||
* @param {string} name
|
||||
* @param {*} status
|
||||
*/
|
||||
constructor(threadid, name, status) {
|
||||
this.threadid = threadid;
|
||||
this.name = name;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
class MethodInvokeArgs {
|
||||
/**
|
||||
* @param {JavaObjectID} objectid
|
||||
* @param {JavaThreadID} threadid
|
||||
* @param {DebuggerMethodInfo} method
|
||||
* @param {DebuggerValue[]} args
|
||||
*/
|
||||
constructor(objectid, threadid, method, args) {
|
||||
this.objectid = objectid;
|
||||
this.threadid = threadid;
|
||||
this.method = method;
|
||||
this.args = args;
|
||||
this.promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
class VariableValue {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {string} [type]
|
||||
* @param {number} [variablesReference]
|
||||
* @param {string} [evaluateName]
|
||||
*/
|
||||
constructor(name, value, type = '', variablesReference = 0, evaluateName = '') {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
this.variablesReference = variablesReference;
|
||||
this.evaluateName = evaluateName;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AttachBuildInfo,
|
||||
BreakpointLocation,
|
||||
BreakpointOptions,
|
||||
DebuggerBreakpoint,
|
||||
DebuggerException,
|
||||
DebuggerFrameInfo,
|
||||
DebuggerMethodInfo,
|
||||
DebuggerTypeInfo,
|
||||
DebugSession,
|
||||
DebuggerValue,
|
||||
LaunchBuildInfo,
|
||||
LiteralValue,
|
||||
JavaBreakpointEvent,
|
||||
JavaExceptionEvent,
|
||||
JavaTaggedValue,
|
||||
JavaType,
|
||||
JavaArrayType,
|
||||
JavaClassType,
|
||||
JavaPrimitiveType,
|
||||
JavaThreadInfo,
|
||||
MethodInvokeArgs,
|
||||
SourceLocation,
|
||||
TypeNotAvailable,
|
||||
VariableValue,
|
||||
}
|
||||
3304
src/debugger.js
3304
src/debugger.js
File diff suppressed because it is too large
Load Diff
109
src/expression/assign.js
Normal file
109
src/expression/assign.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const { Debugger } = require('../debugger');
|
||||
const { DebuggerValue, JavaTaggedValue, JavaType } = require('../debugger-types');
|
||||
const { NumberBaseConverter } = require('../utils/nbc');
|
||||
|
||||
const validmap = {
|
||||
B: 'BC', // char might not fit into a byte - we special-case this
|
||||
S: 'BSC',
|
||||
I: 'BSIC',
|
||||
J: 'BSIJC',
|
||||
F: 'BSIJCF',
|
||||
D: 'BSIJCFD',
|
||||
C: 'BSC',
|
||||
Z: 'Z',
|
||||
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the value will fit into a variable with given type
|
||||
* @param {JavaType} variable_type
|
||||
* @param {DebuggerValue} value
|
||||
*/
|
||||
function checkPrimitiveSize(variable_type, value) {
|
||||
// variable_type_signature must be a primitive
|
||||
if (!Object.prototype.hasOwnProperty.call(validmap, variable_type.signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let value_type_signature = value.type.signature;
|
||||
if (value.vtype === 'literal' && /[BSI]/.test(value_type_signature)) {
|
||||
// for integer literals, find the minimum type the value will fit into
|
||||
if (value.value >= -128 && value.value <= 127) value_type_signature = 'B';
|
||||
else if (value.value >= -32768 && value.value <= 32767) value_type_signature = 'S';
|
||||
else if (value.value >= -2147483648 && value.value <= 2147483647) value_type_signature = 'I';
|
||||
}
|
||||
|
||||
let is_in_range = validmap[variable_type.signature].indexOf(value_type_signature) >= 0;
|
||||
|
||||
// special check to see if a char value fits into a single byte
|
||||
if (JavaType.isByte(variable_type) && JavaType.isChar(value.type)) {
|
||||
is_in_range = validmap.isCharInRangeForByte(value.value);
|
||||
}
|
||||
|
||||
return is_in_range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Debugger} dbgr
|
||||
* @param {DebuggerValue} destvar
|
||||
* @param {string} name
|
||||
* @param {DebuggerValue} result
|
||||
*/
|
||||
async function assignVariable(dbgr, destvar, name, result) {
|
||||
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
||||
throw new Error(`The value is read-only and cannot be updated.`);
|
||||
}
|
||||
|
||||
// non-string reference types can only set to null
|
||||
if (JavaType.isReference(destvar.type) && !JavaType.isString(destvar.type)) {
|
||||
if (!result.hasnullvalue) {
|
||||
throw new Error('Object references can only be set to null');
|
||||
}
|
||||
}
|
||||
|
||||
// as a nicety, if the destination is a string, stringify any primitive value
|
||||
if (JavaType.isPrimitive(result.type) && JavaType.isString(destvar.type)) {
|
||||
result = await dbgr.createJavaStringLiteral(result.value.toString(), { israw:true });
|
||||
}
|
||||
|
||||
if (JavaType.isPrimitive(destvar.type)) {
|
||||
// if the destination is a primitive, we need to range-check it here
|
||||
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
||||
// weirdness if we allow primitives to be set with out-of-range values
|
||||
const is_in_range = checkPrimitiveSize(destvar.type, result);
|
||||
if (!is_in_range) {
|
||||
throw new Error(`'${result.value}' is not compatible with variable type: ${destvar.type.typename}`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = JavaTaggedValue.from(result, destvar.type.signature);
|
||||
|
||||
if (JavaType.isLong(destvar.type) && typeof data.value === 'number') {
|
||||
// convert ints to hex-string longs
|
||||
data.value = NumberBaseConverter.decToHex(data.value.toString(),16);
|
||||
}
|
||||
|
||||
// convert the debugger value to a JavaTaggedValue
|
||||
let newlocalvar;
|
||||
// setxxxvalue sets the new value and then returns a new local for the variable
|
||||
switch(destvar.vtype) {
|
||||
case 'field':
|
||||
newlocalvar = await dbgr.setFieldValue(destvar.data.objvar, destvar.data.field, data);
|
||||
break;
|
||||
case 'local':
|
||||
newlocalvar = await dbgr.setLocalVariableValue(destvar.data.frame, destvar.data.slotinfo, data);
|
||||
break;
|
||||
case 'arrelem':
|
||||
newlocalvar = await dbgr.setArrayElements(destvar.data.array, parseInt(name, 10), 1, data);
|
||||
newlocalvar = newlocalvar[0];
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported variable type');
|
||||
}
|
||||
|
||||
return newlocalvar;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assignVariable,
|
||||
}
|
||||
1048
src/expression/evaluate.js
Normal file
1048
src/expression/evaluate.js
Normal file
File diff suppressed because it is too large
Load Diff
526
src/expression/parse.js
Normal file
526
src/expression/parse.js
Normal file
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* Operator precedence levels.
|
||||
* Lower number = higher precedence.
|
||||
* Operators with equal precedence are evaluated left-to-right.
|
||||
*/
|
||||
const operator_precedences = {
|
||||
'*': 1, '%': 1, '/': 1,
|
||||
'+': 2, '-': 2,
|
||||
'<<': 3, '>>': 3, '>>>': 3,
|
||||
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
||||
'==': 5, '!=': 5,
|
||||
'&': 6, '^': 7, '|': 8,
|
||||
'&&': 9, '||': 10,
|
||||
'?': 11,
|
||||
'=': 12,
|
||||
}
|
||||
|
||||
const lowest_precedence = 13;
|
||||
|
||||
class ExpressionText {
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
constructor(text) {
|
||||
this.expr = text;
|
||||
this.precedence_stack = [lowest_precedence];
|
||||
}
|
||||
|
||||
get current_precedence() {
|
||||
return this.precedence_stack[0];
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedExpression {
|
||||
}
|
||||
|
||||
class RootExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {string} root_term
|
||||
* @param {string} root_term_type
|
||||
* @param {QualifierExpression[]} qualified_terms
|
||||
*/
|
||||
constructor(root_term, root_term_type, qualified_terms) {
|
||||
super();
|
||||
this.root_term = root_term;
|
||||
this.root_term_type = root_term_type;
|
||||
this.qualified_terms = qualified_terms;
|
||||
}
|
||||
}
|
||||
|
||||
class TypeCastExpression extends ParsedExpression {
|
||||
/**
|
||||
*
|
||||
* @param {ParsedExpression} cast_type
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(cast_type, rhs) {
|
||||
super();
|
||||
this.cast_type = cast_type;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
class BinaryOpExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {ParsedExpression} lhs
|
||||
* @param {string} operator
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(lhs, operator, rhs) {
|
||||
super();
|
||||
this.lhs = lhs;
|
||||
this.operator = operator;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
class UnaryOpExpression extends ParsedExpression {
|
||||
/**
|
||||
* @param {string} operator
|
||||
* @param {ParsedExpression} rhs
|
||||
*/
|
||||
constructor(operator, rhs) {
|
||||
super();
|
||||
this.operator = operator;
|
||||
this.rhs = rhs;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
/**
|
||||
* @param {ParsedExpression} condition
|
||||
*/
|
||||
constructor(condition) {
|
||||
super();
|
||||
this.condition = condition;
|
||||
/** @type {ParsedExpression} */
|
||||
this.ternary_true = null;
|
||||
/** @type {ParsedExpression} */
|
||||
this.ternary_false = null;
|
||||
}
|
||||
}
|
||||
|
||||
class QualifierExpression extends ParsedExpression {
|
||||
|
||||
}
|
||||
|
||||
class ArrayIndexExpression extends QualifierExpression {
|
||||
/**
|
||||
* @param {ParsedExpression} index_expression
|
||||
*/
|
||||
constructor(index_expression) {
|
||||
super();
|
||||
this.indexExpression = index_expression;
|
||||
}
|
||||
}
|
||||
|
||||
class MethodCallExpression extends QualifierExpression {
|
||||
/** @type {ParsedExpression[]} */
|
||||
arguments = [];
|
||||
}
|
||||
|
||||
class MemberExpression extends QualifierExpression {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* @param {number|string} length_or_text
|
||||
*/
|
||||
function strip(e, length_or_text) {
|
||||
if (typeof length_or_text === 'string') {
|
||||
if (!e.expr.startsWith(length_or_text)) {
|
||||
return false;
|
||||
}
|
||||
length_or_text = length_or_text.length;
|
||||
}
|
||||
e.expr = e.expr.slice(length_or_text).trimLeft();
|
||||
for (;;) {
|
||||
const comment = e.expr.match(/(^\/\/.+)|(^\/\*[\d\D]*?\*\/)/);
|
||||
if (!comment) break;
|
||||
e.expr = e.expr.slice(comment[0].length).trimLeft();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
* @returns {(MemberExpression|ArrayIndexExpression|MethodCallExpression)[]}
|
||||
*/
|
||||
function parse_qualified_terms(e) {
|
||||
const res = [];
|
||||
while (/^[([.]/.test(e.expr)) {
|
||||
if (strip(e, '.')) {
|
||||
// member access
|
||||
const name_match = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||
if (!name_match) {
|
||||
return null;
|
||||
}
|
||||
const member = new MemberExpression(name_match[0]);
|
||||
strip(e, member.name.length)
|
||||
res.push(member);
|
||||
}
|
||||
else if (strip(e, '(')) {
|
||||
// method call
|
||||
const call = new MethodCallExpression();
|
||||
if (!strip(e, ')')) {
|
||||
for (let arg; ;) {
|
||||
if ((arg = parse_expression(e)) === null) {
|
||||
return null;
|
||||
}
|
||||
call.arguments.push(arg);
|
||||
if (strip(e, ',')) continue;
|
||||
if (strip(e, ')')) break;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
res.push(call);
|
||||
}
|
||||
else if (strip(e, '[')) {
|
||||
// array index
|
||||
const index_expr = parse_expression(e);
|
||||
if (index_expr === null) {
|
||||
return null;
|
||||
}
|
||||
if (!strip(e, ']')) {
|
||||
return null;
|
||||
}
|
||||
res.push(new ArrayIndexExpression(index_expr));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExpressionText} e
|
||||
*/
|
||||
function parseBracketOrCastExpression(e) {
|
||||
if (!strip(e, '(')) {
|
||||
return null;
|
||||
}
|
||||
let res = parse_expression(e);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
if (!strip(e, ')')) {
|
||||
return null;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
const qt = parse_qualified_terms(e);
|
||||
return new BracketedExpression(res, qt);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ExpressionText} e
|
||||
* @param {string} unop
|
||||
*/
|
||||
function parseUnaryExpression(e, unop) {
|
||||
strip(e, unop.length);
|
||||
let res = parse_expression_term(e);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
const op = unop.replace(/\s+/g, '');
|
||||
for (let i = op.length - 1; i >= 0; --i) {
|
||||
res = new UnaryOpExpression(op[i], res);
|
||||
}
|
||||
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] === '(') {
|
||||
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'];
|
||||
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;
|
||||
}
|
||||
strip(e, root_term[0].length);
|
||||
const root_term_type = root_term_types[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1];
|
||||
const qualified_terms = parse_qualified_terms(e);
|
||||
if (qualified_terms === null) {
|
||||
return null;
|
||||
}
|
||||
return new RootExpression(root_term[0], root_term_type, qualified_terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
*/
|
||||
function getBinaryOperator(s) {
|
||||
const binary_op_match = s.match(/^([!=/%*^+-]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
return binary_op_match ? binary_op_match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
if (!binary_operator) {
|
||||
break;
|
||||
}
|
||||
const prec_diff = operator_precedences[binary_operator] - e.current_precedence;
|
||||
if (prec_diff > 0) {
|
||||
// bigger number -> lower precendence -> end of (sub)expression
|
||||
break;
|
||||
}
|
||||
if (prec_diff === 0 && binary_operator !== '?') {
|
||||
// equal precedence, ltr evaluation
|
||||
break;
|
||||
}
|
||||
// higher or equal precendence
|
||||
e.precedence_stack.unshift(e.current_precedence + prec_diff);
|
||||
strip(e, binary_operator.length);
|
||||
if (binary_operator === '?') {
|
||||
res = new TernaryExpression(res);
|
||||
res.ternary_true = parse_expression(e);
|
||||
if (!strip(e, ':')) {
|
||||
return null;
|
||||
}
|
||||
res.ternary_false = parse_expression(e);
|
||||
} else {
|
||||
res = new BinaryOpExpression(res, binary_operator, parse_expression(e));
|
||||
}
|
||||
e.precedence_stack.shift();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ArrayIndexExpression,
|
||||
BinaryOpExpression,
|
||||
ExpressionText,
|
||||
MemberExpression,
|
||||
MethodCallExpression,
|
||||
NewArrayExpression,
|
||||
NewObjectExpression,
|
||||
parse_expression,
|
||||
ParsedExpression,
|
||||
QualifierExpression,
|
||||
RootExpression,
|
||||
TypeCastExpression,
|
||||
UnaryOpExpression,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user