10 Commits

Author SHA1 Message Date
Dave Holoway
8dbbfa8344 version 1.3.1 2020-07-08 19:10:00 +01:00
Dave Holoway
669ed81f39 Add support for ADB server environment variables (#96)
* support ADB env vars for configuring the server connection

* update startADBServer to use common adb socket params

* use adbSocket config value to set adb host and port

* make sure env var values are trimmed

* pretty-print launch args and env vars

* use adb socket host for JDWP connections

* allow JDWP port to be fixed

* include the command detail in adb command failures

* configure adb socket and jdwp port parameters for attach configs

* bump version 1.3.0
2020-07-08 19:07:30 +01:00
Dave Holoway
bfd55354c7 v1.2.1 (#94)
* update java-mti to support new type cache format

* add IP and platform info to events

* add warning about closing Android Studio if an ADB or JDWP error occurs

* version 1.2.1 notes

* add debug_mode to disconnect event
2020-07-06 17:11:54 +01:00
Dave Holoway
83eda790be version 1.2 (#93)
* initial working language server
* first hacky version of source parsing and type checking
* first iteration of method body parser
* add support for prefix/postfix inc expressions
* add basic support for parsing new expressions
* different attempt to parse using collapsable text ranges
* fix parsing of binary operstors following a bracket expression
* updated validation to use new JavaTypes module instead of MTIs
* add support for array-literal expressions
* fix || and && not being tokenized as operators allow float literals starting with dot
* add new method body parser to use direct linear parsing
* add super as an object literal
* fix interface constructors check constructor type modifiers
* fix assignment operator types
* Fix resolving of enclosed type identifiers
* add default constructor for class types with no explicit constructors
* add missing constructor validator
* add constructor parameters to list of resolvable types
* update SourceMethod to pass name in super constructor
* add Any* classes to reduce cascading errors
* update method call parameter checking use isTypeAssignable instead of getParameterCompatibleTypeSignatures
* tidy up isTypeAssignable allow class equivilents for primitives
* add more info when methods/ctrs cannot be matched
* allow interfaces to be cast to class instances
* use isTypeAssignable for checking branch test expressions
* allow AnyValue to be a constant value
* split shift operators from bitwise operators
* add support for literal numbers to be assignable to multiple primtive types
* clear diagnostics when document is closed
* update check for cast expression
* casting only applies to qualified term not a whole expression
* allow all primitive-number-type casts
* add support for synchronized statement
* update primitive type compatibility
* allow null to be cast to any non-primitve
* use better regex for string literals
* allow character literals to be assigned to number types
* add support for array qualifiers after a variable name
* make sure any long specifier is stripped from a bigint value
* improve invalid array expression message add AnyType array element to prevent cascading errors
* make default a modifer keyword for interface default method support
* initial support for wildcard type arguments
* fix parse issue with nested generic types
* allow generic types to be assigned to inherited types with compatible type arguments
* allow unicode characters, $ and _ in identifiers
* map primitive types to their boxed versions for class member
* support assert statement
* allow unicode char literals
* make type parser and body parser use same tokenizer
* reuse parsed tokens instead of tokenizing each method body
* re-add throws as a keyword
* treat default and synchronized as modifiers
* add SourceInitialiser support
* refactor to prepare for merging with type parsing
* add support for array qualifiers in type identifiers
* pass scoped type instead of method to typeIdent
* update ResolvableType to use same type resolving as method body parsing
* add support for post-name array qualifiers in fields and parameters
* post-name array qualifiers in method decls
* add type variables to SourceMethod
* initial attempt to support type variable arguments in methods
* specialise methods with type variables
* don't require default interface methods to be implemented
* make variable arity parameters an array type
* tidy array constructors and fix some warnings
* update isCallCompatible to handle variable arity calls
* improve assert statement support
* parse labels and break/continue targets
* refactor new term qualifiers
* add support for generic inferred-type arguments
* improve modifier checks for interface types
* improve reporting of unresolved type errors
* fix type checking of field and method declarations
* add missng strictfp modifier
* refactor in preparation for parsing local types
* replace Locals with scopeable MethodDeclarations to allow labels and types to be stored
* initial changes to support local type declarations
* update to use new set of SourceX classes
* refactor to allow expressions to have a type scope
* replace regex parsing with linear parsing
* generate source types before parsing
* fix support for resolving type variables in method declarations
* fix checking of array literal compatability
* report errors from unit parsing
* remove local modifier validation during parse add parameter modifier checking to validation
* allow trailing comma for array literals
* start separating validation from parsing
* add support for parsing enum values
* allow uppercase 0X in hex literals
* include enclosing types in identifier search
* add support for parsing parameterless lambdas
* ignore unresolved types in extends/implements
* implement specialisation of SourceType
* allow super as a member qualifier
* allow empty enums
* don't report missing constructors if superclass has none
* update typemap declarations to use CEIType instead of JavaType
* fix resolving of class type variables
* fix bad imports when resolving annotations
* allow null scope in findIdentifier
* add support for static member imports
* import types from same package
* remove this qualifier from isCastExpression
* add hex exponent support
* parse try-with-resources
* fix resolving imported enclosed types
* extract expression types into separate files
* extract statement types into separate files
* fix type warnings
* extract literals into separate files
* remove Value class, add NewExpression and separate out Any classes
* rename source types module
* remove some parse checks that should be in verify
* support token extraction in expressions
* implement resolveExpression
* add type cast checking
* check for valid type in class member expressions
* allow assigns for assignable type arguments
* improve reporting of unresolved identifiers
* add new array validation
* validate array literals
* validate array indexes
* improve validation of binary operators
* rename ResolvedType  to ResolvedValue
* improve checking of number literals
* support package name as a resolved value
* implement method body and ststement validation
* improve method call resolving
* add support for this() and super() constructor calls
* remove return type for source constructors
* add checks for unary operators
* ensure tokens are assigned for qualified expressions
* check castability using type assignments
* add implicit enum methods values() and valueOf()
* add basic type checking of lambda expressions
* fix return type check
* fix assert statement checks
* improve support for ternary operators in assignments and method invocations
* perform more detailed search of implemented methods
* initial test of context-dependant code completion
* support package, type and static field import completion
* support for member expressions
* use exact type signatures for locating types for completion items
* add support for field and method docs
* add support for docs in source types
* support member completion for array types improve comment formatting
* ensure Object is always last in the list of inherited types
* add owning method to statements create common keyword statement class
* improve code completion list add method parameters order list items by scope
* add source types to list hide this and super for non-methods
* fix bad member resolution at end of block fix missing method and type docs
* add support for editing multiple files
* allow multiple source files to be used in parsing
* load and parse files at startup
* add support for displaying method signatures
* add single trace function with timestamps
* implement shceduleReparse to reduce parsing load while typing
* remove parsed type list logging
* wait for reparsing before returning method signatures
* resolve new object contructors
* improve extraction of parameter docs
* update @types/vscode
* cache decoded android library in globalStoragePath
* load single android library cache from local folder
* android-29 library cache
* allow configurable app root setting
* set configurable trace logging and update section names
* description updates
* handle null token passed to ParseProblem
* refactoring
* Rename language client extension to Android
* ignore unnamed type declarations
* handle java file change notifications
* make sure we only try and parse java files
* add option to allow language server to be shutdown
* simplify handling of this and class member qualifiers
* relocate java-mti package into project
* get main node install to install langserver dependencies
* remove debugging pause
* rename body-parser3 to body-parser
* clean up import resolving code
* remove unused field from ResolvedImport
* remove validation modules that used old parser types
* remove old parser files
* remove redundant types and functions used by old parser
* move addproblem into TokenList
* remove unused ResolvedType class
* validate more statements
* add support for parsing and validating anonymous types
* hide some method modifiers which aren't useful to show
* code comments and minor improvements
* fix some type warnings
* improve support for completion of enum values
* add type name to parameter completion labels
* ignore synthetic members in completion list
* use a specialised map for handling case-insenstive file uris
* add basic build script
* reference java-mti package from GitHub
* revert @types/vscode
* update initial file loading to use URIs passed from the client changes to the appSourceRoot now require an extension restart
* add support for loading filtered androidx libraries for code completion
* update version of java-mti
* add mixpanel package
* add basic analytics
* fix dependency versions
* fix dependency versions
* set empty cache file markers
* add language server debug config
* add file to build script
* add unqualified type members when inside a method
* apply statics filter to enum values
* add basic debugger analytics
* include current time in startup event
* add terminate reason to debugger
* update changelog and readme
2020-07-03 01:54:32 +01:00
Dave Holoway
d064b9a3f4 version 1.1 2020-04-24 19:19:21 +01:00
Dave Holoway
6439e1b8b7 Version 1.1 improvements (#88)
* fix 0 alignment in binary xml decoding

* output reason for APK manifest read failure

* try and match package name against process name
when determining which pid to attach

* make post launch pause user-configurable

* code tidy, jsdocs and types

* more types in expression parse classes

* fix issue with expandable objects not evaluating

* update build task example

* fix package/type evaluation

* improve handling of targetDevice and processID combinations

* show full call stack by default

* implement a queue for evaluations

* improve performance of retrieving single fields

* check root term identifiers against this fields
2020-04-24 19:03:39 +01:00
Dave Holoway
a4ce09d309 Expression format specifier support (#87)
* initial support for format specifier

* add readme notes for format specifiers

* add support for showing arrays and objects  with format specifiers

* create unique object variable references for different display formats

* add notes on applying formatting to objects and arrays
2020-04-24 14:37:51 +01:00
Dave Holoway
1535f133d9 Support for device picker during launch (#86) 2020-04-23 16:44:19 +01:00
Dave Holoway
44d887dd6c Support attaching to running app (#85)
* add support for timeout on adb socket reads

* add debugger support for attaching to a process

* add new launch configuration and support for picking an Android process ID

* initial support for attaching to android process

* display enhanced quick pick list with pids and names

* add flag to prevent disconnect messages when not connected

* Retrieve all loaded classes during startup.
This allows us to identify breakpoints in anonymous classes that are already loaded.

* correct name of process picker command

* make PickAndroidProcess command private

* selectAndroidProcessID always returns an object

* make breakpoint setup a loop instead of recursive

* tidy some labels and error messages

* use a more consistent command for retrieving process names

* show pid list sorted by pid instead of name

* refactor some Android and ADB-specific functions
Check ANDROID_SDK as replacement for ANDROID_HOME

* tidy up logcat launch and refactor target device selection

* fix logcat not displaying

* filter duplicates and blanks from logcat output
2020-04-23 13:28:03 +01:00
Dave Holoway
9aeca6b96b support custom launch arguments (#84)
* add amStartArgs launch property to allow launch command arguments to be customised.

* update readme launch options docs
2020-04-23 13:27:17 +01:00
105 changed files with 11978 additions and 567 deletions

20
.vscode/launch.json vendored
View File

@@ -14,7 +14,17 @@
]
},
{
"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}",
@@ -35,8 +45,12 @@
],
"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" ]
}
]
}

View File

@@ -1,5 +1,24 @@
# Change Log
### 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

125
README.md
View File

@@ -2,6 +2,9 @@
This is a preview version of the Android for VS Code Extension. The extension allows developers to install, launch and debug Android Apps from within the VS Code environment.
## What's New
- Java Intellisense for Android is now in beta.
## Features
* Line by line code stepping
* Breakpoints
@@ -18,12 +21,11 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
## Limitations
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension **will not build your app**.
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug`.
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode task to automatically build your app before launching a debug session.
* This extension will not build your app.
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug` or configure a VSCode Build Task to run the command (see below).
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode Task to automatically build your app before launching a debug session.
* Some debugger options are yet to be implemented. You cannot set conditional breakpoints and watch expressions must be simple variables.
* If you require a must-have feature that isn't there yet, let us know on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension does not provide any additional code completion or other editing enhancements.
## Extension Settings
@@ -34,31 +36,65 @@ The following settings are used to configure the debugger:
"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",
// Fully qualified path to the AndroidManifest.xml file compiled in the APK. Default: appSrcRoot/AndroidManifest.xml
// 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",
// APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: ["-r"]
// Custom arguments passed to the Android package manager to install the app.
// Run 'adb shell pm' to show valid arguments. Default: ["-r"]
"pmInstallArgs": ["-r"],
// Manually specify the activity to run when the app is started.
"launchActivity": ".MainActivity"
// 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
}
]
}
@@ -79,6 +115,7 @@ Add a `preLaunchTask` item to the launch configuration:
"request": "launch",
"name": "App Build & Launch",
"preLaunchTask": "run gradle",
...
}
]
}
@@ -93,12 +130,70 @@ 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).
![Java Intellisense](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/java-intellisense.png)
## 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

View File

@@ -1,22 +1,170 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const path = require('path');
const vscode = require('vscode');
const analytics = require('./langserver/analytics');
const package_json = require('./package.json');
const { LanguageClient, TransportKind, } = require('vscode-languageclient');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
const { selectAndroidProcessID } = require('./src/process-attach');
const { selectTargetDevice } = require('./src/utils/device');
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
/**
* @param {vscode.ExtensionContext} context
*/
async function createLanguageClient(context) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js'));
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
/** @type {import('vscode-languageclient').ServerOptions} */
let serverOptions = {
run: {
module: serverModule,
transport: TransportKind.ipc,
},
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions
}
};
const config = vscode.workspace.getConfiguration('android-dev-ext');
const appSourceRoot = config.get('appSourceRoot', '');
let globSearchRoot = appSourceRoot;
if (globSearchRoot) {
// for findFiles to work properly, the path cannot begin with slash or have any relative components
globSearchRoot = path.normalize(appSourceRoot.replace(/(^[\\/]+)|([\\/]+$)/,''));
if (globSearchRoot) globSearchRoot += '/';
}
const sourceFiles = (await vscode.workspace.findFiles(`${globSearchRoot}**/*.java`, null, 1000, null)).map(uri => uri.toString());
const mpids = analytics.getIDs(context);
// Options to control the language client
/** @type {import('vscode-languageclient').LanguageClientOptions} */
let clientOptions = {
// Register the server for Java source documents
documentSelector: [{
scheme: 'file', language: 'java'
}],
initializationOptions: {
// extensionPath points to the root of the extension (the folder where this file is)
extensionPath: context.extensionPath,
mpuid: mpids.uid,
mpsid: mpids.sid,
initialSettings: config,
sourceFiles,
vscodeVersion: vscode.version,
workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()),
},
synchronize: {
// Notify the server about file changes to '.java files contained in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.java')
},
};
// Create the language client - this won't do anything until start() is called
return new LanguageClient(
'androidJavaLanguageServer',
'Android',
serverOptions,
clientOptions
);
}
/** @type {LanguageClient} */
let languageClient;
let languageSupportEnabled = false;
function refreshLanguageServerEnabledState() {
if (!languageClient) {
return;
}
let langSupport = vscode.workspace.getConfiguration('android-dev-ext').get('languageSupport', false);
if (langSupport === languageSupportEnabled) {
return;
}
if (langSupport) {
if (languageClient.needsStart()) {
languageClient.start();
}
languageSupportEnabled = true;
} else {
if (languageClient.needsStop()) {
languageClient.stop().then(() => {
languageSupportEnabled = false;
refreshLanguageServerEnabledState();
});
}
}
}
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
AndroidContentProvider.register(context, vscode.workspace);
const mpids = analytics.getIDs(context);
analytics.init(undefined, mpids.uid, mpids.sid, package_json, { vscode_version: vscode.version });
createLanguageClient(context).then(client => {
languageClient = client;
refreshLanguageServerEnabledState();
});
// The commandId parameter must match the command field in package.json
const disposables = [
// add the view logcat handler
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
openLogcatWindow(vscode);
}),
// 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 => {
// perform the refresh on the next tick to prevent spurious errors when
// trying to shut down the language server in the middle of a change-configuration request
process.nextTick(() => refreshLanguageServerEnabledState());
}),
];
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

179
langserver/analytics.js Normal file
View File

@@ -0,0 +1,179 @@
const os = require('os');
let mp;
/** @type {string} */
let uid;
/** @type {string} */
let sid;
/** @type {Map<string,[number,number]>} */
const timeLabels = new Map();
let session_start = Date.now();
/** @type {string|Promise<string>} */
let ip = getCurrentIP()
.catch(() => null)
.then(res => ip = res);
let queued_events = null;
/**
* @param {string} [t]
* @param {string} [u]
* @param {string} [s]
* @param {{name:string,version:string}} [package_json]
* @param {*} [props]
*/
function init(t = '0cca95950055c6553804a46ce7e3df18', u, s, package_json, props) {
if (mp) {
return;
}
try {
mp = require('mixpanel').init(t);
}
catch {
return;
}
uid = u;
sid = s;
if (!props) {
return;
}
const now = new Date();
event(`${package_json.name}-start`, {
extension: package_json.name,
ext_version: package_json.version,
arch: process.arch,
cpus: os.cpus().length,
mem: (os.totalmem() / 1e6)|0,
platform: process.platform,
node_version: process.version,
release: os.release(),
localtime: now.toTimeString(),
tz: now.getTimezoneOffset(),
...props
});
}
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 (!mp) {
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 {
if (uid) {
mp.track(eventName, {
ip,
distinct_id: uid,
session_id: sid,
session_length: Math.trunc((Date.now() - session_start) / 60e3),
...properties,
});
} else {
mp.track(eventName, {
ip,
platform: process.platform,
release: os.release(),
node_version: process.version,
...properties,
});
}
} catch {}
}
/**
* @param {string} label
*/
function time(label) {
if (!label || timeLabels.has(label)) {
return;
}
timeLabels.set(label, process.hrtime());
}
/**
* @param {string} label
* @param {'ns'|'us'|'ms'|'s'} time_unit
* @param {*} [additionalProps]
*/
function timeEnd(label, time_unit = 'ms', additionalProps = {}) {
if (!label) {
return;
}
const startTime = timeLabels.get(label);
timeLabels.delete(label);
if (!Array.isArray(startTime)) {
return;
}
const elapsed = process.hrtime(startTime);
const count = time_unit === 's' ? elapsed[0] : ((elapsed[0]*1e9) + elapsed[1]);
const divs = {
ns: 1, us: 1e3, ms: 1e6, s: 1
}
const props = {
[`${label}-elapsed`]: Math.trunc(count / (divs[time_unit] || 1)),
[`${label}-elapsed_unit`]: time_unit,
...additionalProps,
}
event(label, props);
}
/**
* @param {import('vscode').ExtensionContext} context
*/
function getIDs(context) {
if (!context || !context.globalState) {
return {
uid: '', sid: ''
};
}
let uuidv4 = () => {
try {
uuidv4 = require('uuid').v4;
return uuidv4();
} catch {
return '';
}
}
let u = uid || (uid = context.globalState.get('mix-panel-id'));
if (typeof u !== 'string' || u.length > 36) {
u = uid = uuidv4();
context.globalState.update('mix-panel-id', u);
}
let s = sid || (sid = uuidv4());
return {
uid: u,
sid: s,
}
}
exports.init = init;
exports.event = event;
exports.time = time;
exports.timeEnd = timeEnd;
exports.getIDs = getIDs;

113
langserver/build.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* This is a really basic module packer. It simply loads all the modules into a
* single object, appends a simple bootstrap require function to 'load' the modules at
* runtime and then writes the result out to a entry module.
*
* - Each local module (i.e not in node_modules) must be a relative path starting with . or ..
* - Subfolders are allowed, but only js files are included.
*/
/** These are the sources (files and folders) we want to pack */
const sources = [
'analytics.js',
'completions.js',
'doc-formatter.js',
'document.js',
'java',
'logging.js',
'method-signatures.js',
'server.js',
'settings.js'
]
/** The entry module - must have a relative path */
const entry = './server.js';
const fs = require('fs');
const modules = [];
while (sources.length) {
const source = sources.shift();
const stat = fs.statSync(source);
if (stat.isDirectory()) {
fs.readdirSync(source).forEach(entry => {
sources.unshift(`${source}/${entry}`);
})
continue;
}
if (!source.endsWith('.js')) {
console.log(`ignoring non-js file: ${source}`);
continue;
}
// add an object entry of the form: 'path': (...) => { file_content }
modules.push(`'${source}':
(require,module,exports) => {
${fs.readFileSync(source, 'utf8')}
}`
);
}
/**
* The bootstrap contains the custom require function and the call to load
* the initial module - it's everything after the marker below
*/
const bootstrap = fs.readFileSync(__filename, 'utf8').split('/* bootstrap marker */').pop();
fs.writeFileSync(entry,
`const data = {
${modules.join(',\n')}
}
${bootstrap}
_require('${entry}');
`);
/* bootstrap marker */
const module_stack = [{
path: [],
name: '',
}]
const loadedModules = new Set();
function _require(filename) {
// local modules always have a relative path
if (!filename.startsWith('.')) {
// node_modules import
return require(filename);
}
const new_path = module_stack[0].path.slice();
let key = filename.replace(/(\.js)?$/, '.js');
for (let m; m = key.match(/^\.\.?\//);) {
key = key.slice(m[0].length);
if (m[0] === '../') {
new_path.pop();
}
}
key = [...new_path, key].join('/');
if (!Object.prototype.hasOwnProperty.call(data, key)) {
throw new Error(`Missing module: ${key}`);
}
const entry = data[key];
if (loadedModules.has(key)) {
return entry;
}
const path_parts = key.split(/[\\/]/);
module_stack.unshift({
name: path_parts.pop(),
path: path_parts,
})
const mod = {
exports: {},
}
entry(_require, mod, mod.exports);
module_stack.shift();
loadedModules.add(key);
return data[key] = mod.exports;
}

514
langserver/completions.js Normal file
View File

@@ -0,0 +1,514 @@
const { JavaType, CEIType, ArrayType, PrimitiveType } = require('java-mti');
const { getTypeInheritanceList } = require('./java/expression-resolver');
const { CompletionItem, CompletionItemKind } = require('vscode-languageserver');
const { SourceType } = require('./java/source-types');
const { indexAt } = require('./document');
const { formatDoc } = require('./doc-formatter');
const { trace } = require('./logging');
const { event } = require('./analytics');
/**
* Case-insensitive sort routines
*/
const sortBy = {
label: (a,b) => a.label.localeCompare(b.label, undefined, {sensitivity: 'base'}),
name: (a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}),
}
/** Map Java typeKind values to vscode CompletionItemKinds */
const TypeKindMap = {
class: CompletionItemKind.Class,
interface: CompletionItemKind.Interface,
'@interface': CompletionItemKind.Interface,
enum: CompletionItemKind.Enum,
};
/**
* Return a list of vscode-compatible completion items for a given type.
*
* The type is located in typemap and the members (fields, methods) are retrieved
* and converted to completions items.
*
* @param {Map<string,CEIType>} typemap Set of known types
* @param {string} type_signature Type to provide completion items for
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
* @param {string[]} [typelist] optional pre-prepared type list (to save recomputing it)
*/
function getTypedNameCompletion(typemap, type_signature, opts, typelist) {
let type, types, subtype_search;
const arr_match = type_signature.match(/^\[+/);
if (arr_match) {
// for arrays, just create a dummy type
types = [
type = new ArrayType(PrimitiveType.map.V, arr_match[0].length),
typemap.get('java/lang/Object'),
];
} else if (!/^L.+;/.test(type_signature)) {
return [];
} else {
type = typemap.get(type_signature.slice(1,-1));
if (!type) {
return [];
}
if (!(type instanceof CEIType)) {
return [];
}
// retrieve the complete list of inherited types
types = getTypeInheritanceList(type);
subtype_search = type.shortSignature + '$';
}
class SetOnceMap extends Map {
set(key, value) {
return this.has(key) ? this : super.set(key, value);
}
}
const fields = new SetOnceMap(),
methods = new SetOnceMap(),
inner_types = new SetOnceMap(),
enumValues = new SetOnceMap();
/**
* @param {string[]} modifiers
* @param {JavaType} t
* @param {boolean} [synthetic]
*/
function shouldInclude(modifiers, t, synthetic) {
// filter statics/instances
if (opts.statics !== modifiers.includes('static')) return;
// exclude synthetic entries
if (synthetic) return;
if (modifiers.includes('public')) return true;
if (modifiers.includes('protected')) return true;
// only include private items for the current type
if (modifiers.includes('private') && t === type) return true;
// @ts-ignore
return t.packageName === type.packageName;
}
// retrieve fields and methods
types.forEach(t => {
if (t instanceof SourceType && opts.statics) {
t.enumValues.sort(sortBy.name)
.forEach(e => enumValues.set(e.name, {e, t}))
}
t.fields.sort(sortBy.name)
.filter(f => shouldInclude(f.modifiers, t, f.isSynthetic))
.forEach(f => {
if (f.isEnumValue) {
enumValues.set(f.name, {e:f, t});
} else {
fields.set(f.name, {f, t});
}
});
t.methods.sort(sortBy.name)
.filter(m => shouldInclude(m.modifiers, t, m.isSynthetic))
.forEach(m => methods.set(`${m.name}${m.methodSignature}`, {m, t}));
});
if (opts.statics && subtype_search) {
// retrieve inner types
(typelist || [...typemap.keys()])
.filter(type_signature =>
type_signature.startsWith(subtype_search)
// ignore inner-inner types
&& !type_signature.slice(subtype_search.length).includes('$')
)
.map(type_signature => typemap.get(type_signature))
.forEach((t,idx) => inner_types.set(t.simpleTypeName, { t }));
}
return [
// enum values
...[...enumValues.values()].map((e,idx) => ({
label: `${e.e.name}: ${e.t.simpleTypeName}`,
insertText: e.e.name,
kind: CompletionItemKind.EnumMember,
sortText: `${idx+1000}${e.e.name}`,
data: { type: e.t.shortSignature, fidx: e.t.fields.indexOf(e.e) },
})),
// fields
...[...fields.values()].map((f,idx) => ({
label: `${f.f.name}: ${f.f.type.simpleTypeName}`,
insertText: f.f.name,
kind: CompletionItemKind.Field,
sortText: `${idx+2000}${f.f.name}`,
data: { type: f.t.shortSignature, fidx: f.t.fields.indexOf(f.f) },
})),
// methods
...[...methods.values()].map((m,idx) => ({
label: m.m.shortlabel,
kind: CompletionItemKind.Method,
insertText: m.m.name,
sortText: `${idx+3000}${m.m.name}`,
data: { type: m.t.shortSignature, midx: m.t.methods.indexOf(m.m) },
})),
// types
...[...inner_types.values()].map((it,idx) => ({
label: it.t.simpleTypeName,
kind: TypeKindMap[it.t.typeKind],
sortText: `${idx+4000}${it.t.simpleTypeName}`,
data: { type: it.shortSignature },
})),
]
}
/**
* Return a list of vscode-compatible completion items for a dotted identifier (package or type).
*
* @param {Map<string,CEIType>} typemap Set of known types
* @param {string} dotted_name
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
*/
function getFullyQualifiedDottedIdentCompletion(typemap, dotted_name, opts) {
if (dotted_name === '') {
// return the list of top-level package names
return getTopLevelPackageCompletions(typemap);
}
// name is a fully dotted name, possibly including members and their fields
let typelist = [...typemap.keys()];
const split_name = dotted_name.split('.');
let pkgname = '';
/** @type {JavaType} */
let type = null, typename = '';
for (let name_part of split_name) {
if (type) {
if (opts.statics && typelist.includes(`${typename}$${name_part}`)) {
type = typemap.get(typename = `${typename}$${name_part}`);
continue;
}
break;
}
typename = pkgname + name_part;
if (typelist.includes(typename)) {
type = typemap.get(typename);
continue;
}
pkgname = `${pkgname}${name_part}/`;
}
if (type) {
return getTypedNameCompletion(typemap, type.typeSignature, opts, typelist);
}
// sub-package or type
const search_pkg = pkgname;
return typelist.reduce((arr,typename) => {
if (typename.startsWith(search_pkg)) {
const m = typename.slice(search_pkg.length).match(/^(.+?)(\/|$)/);
if (m) {
if (m[2]) {
// package name
if (!arr.find(x => x.label === m[1])) {
arr.push({
label: m[1],
kind: CompletionItemKind.Unit,
data: null,
})
}
} else {
// type name
arr.push({
label: m[1].replace(/\$/g,'.'),
kind: CompletionItemKind.Class,
data: { type: typename },
})
}
}
}
return arr;
}, []);
}
/**
* Return a list of completion items for top-level package names (e.g java, javax, android)
*
* @param {Map<string,CEIType>} typemap
*/
function getTopLevelPackageCompletions(typemap) {
const pkgs = [...typemap.keys()].reduce((set, short_type_signature) => {
// the root package is the first part of the short type signature (up to the first /)
const m = short_type_signature.match(/(.+?)\//);
m && set.add(m[1]);
return set;
}, new Set());
const items = [...pkgs].filter(x => x)
.sort()
.map(package_ident => ({
label: package_ident,
kind: CompletionItemKind.Unit,
sortText: package_ident,
}));
return items;
}
/**
* @param {Map<string,CEIType>} typemap
* @param {string} pkg
*/
function getPackageCompletion(typemap, pkg) {
if (pkg === '') {
return getTopLevelPackageCompletions(typemap);
}
// sub-package
const search_pkg = pkg + '/';
const pkgs = [...typemap.keys()].reduce((arr,typename) => {
if (typename.startsWith(search_pkg)) {
const m = typename.slice(search_pkg.length).match(/^(.+?)\//);
if (m) arr.add(m[1]);
}
return arr;
}, new Set());
return [...pkgs].filter(x => x).sort().map(pkg => ({
label: pkg,
kind: CompletionItemKind.Unit,
data: null,
}));
}
/** Cache of completion items for fixed values, keywords and Android library types */
let defaultCompletionTypes = null;
/** @type {Map<string,CEIType>} */
let lastCompletionTypeMap = null;
let completionRequestCount = 0;
function initDefaultCompletionTypes(lib) {
defaultCompletionTypes = {
instances: 'this super'.split(' ').map(t => ({
label: t,
kind: CompletionItemKind.Value,
sortText: t
})),
// primitive types
primitiveTypes:'boolean byte char double float int long short void'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
sortText: t,
})),
// modifiers
modifiers: 'public private protected static final abstract volatile native transient strictfp synchronized'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
sortText: t,
})),
// literals
literals: 'false true null'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Value,
sortText: t
})),
// type names
types: [...lib.values()].map(
t =>
/** @type {CompletionItem} */
({
label: t.dottedTypeName,
kind: TypeKindMap[t.typeKind],
data: { type: t.shortSignature },
sortText: t.dottedTypeName,
})
).sort(sortBy.label),
// package names
packageNames: getTopLevelPackageCompletions(lib),
}
}
function clearDefaultCompletionEntries() {
defaultCompletionTypes = null;
}
/**
* Called from the VSCode completion item request.
*
* @param {import('vscode-languageserver').CompletionParams} params
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
*/
async function getCompletionItems(params, liveParsers, androidLibrary) {
trace('getCompletionItems');
if (!params || !params.textDocument || !params.textDocument.uri) {
return [];
}
let dct = defaultCompletionTypes;
if (!defaultCompletionTypes) {
initDefaultCompletionTypes(androidLibrary);
dct = defaultCompletionTypes || {};
}
// wait for the Android library to load (in case we receive an early request)
if (androidLibrary instanceof Promise) {
androidLibrary = await androidLibrary;
}
// retrieve the parsed source corresponding to the request URI
const docinfo = liveParsers.get(params.textDocument.uri);
if (!docinfo || !docinfo.parsed) {
return [];
}
// wait for the user to stop typing
const preversion = docinfo.version;
await docinfo.reparseWaiter;
if (docinfo.version !== preversion) {
// if the file content has changed since this request wss made, ignore it
trace('content changed - ignoring completion items')
/** @type {import('vscode-languageserver').CompletionList} */
return {
isIncomplete: true,
items: [],
}
}
completionRequestCount += 1;
if ((completionRequestCount === 1) || (completionRequestCount === 5) || ((completionRequestCount % 25) === 0)) {
event('completion-requests', { comp_req_count: completionRequestCount });
}
let parsed = docinfo.parsed;
// save the typemap associated with this parsed state - we use this when resolving
// the documentation later
lastCompletionTypeMap = (parsed && parsed.typemap) || androidLibrary;
let locals = [],
modifiers = dct.modifiers,
type_members = [],
sourceTypes = [];
if (parsed.unit) {
const char_index = indexAt(params.position, parsed.content);
const options = parsed.unit.getCompletionOptionsAt(char_index);
if (options.loc) {
if (/^pkgname:/.test(options.loc.key)) {
return getPackageCompletion(parsed.typemap, options.loc.key.split(':').pop());
}
if (/^fqdi:/.test(options.loc.key)) {
// fully-qualified dotted identifier
return getFullyQualifiedDottedIdentCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
}
if (/^fqs:/.test(options.loc.key)) {
// fully-qualified static expression
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
}
if (/^fqi:/.test(options.loc.key)) {
// fully-qualified instance expression
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: false });
}
}
// if this token is inside a method, include the parameters and this/super
if (options.method) {
locals = options.method.parameters
.sort(sortBy.name)
.map(p => ({
label: `${p.name}: ${p.type.simpleTypeName}`,
insertText: p.name,
kind: CompletionItemKind.Variable,
sortText: p.name,
}));
// if this is not a static method, include this/super
if (!options.method.modifiers.includes('static')) {
locals.push(...dct.instances);
}
type_members = getTypedNameCompletion(
parsed.typemap,
options.method.owner.typeSignature,
{ statics: !!options.method.modifierTokens.find(m => m.value === 'static') }
);
// if we're inside a method, don't show the modifiers
modifiers = [];
}
}
// add types currently parsed from the source files
liveParsers.forEach(doc => {
if (!doc.parsed) {
return;
}
doc.parsed.unit.types.forEach(
t => sourceTypes.push({
label: t.dottedTypeName,
kind: TypeKindMap[t.typeKind],
data: { type:t.shortSignature },
sortText: t.dottedTypeName,
})
)
});
// exclude dotted (inner) types because they result in useless
// matches in the intellisense filter when . is pressed
const types = [
...dct.types,
...sourceTypes,
].filter(x => !x.label.includes('.'))
.sort(sortBy.label)
return [
...locals,
...type_members,
...dct.primitiveTypes,
...dct.literals,
...modifiers,
...types,
...dct.packageNames,
].map((x,idx) => {
// to force the order above, reset sortText for each item based upon a fixed-length number
x.sortText = `${1000+idx}`;
return x;
})
}
/**
* Set the detail and documentation for the specified item
*
* @param {CompletionItem} item
*/
function resolveCompletionItem(item) {
item.detail = item.documentation = '';
if (!lastCompletionTypeMap) {
return item;
}
if (!item.data || typeof item.data !== 'object') {
return item;
}
const type = lastCompletionTypeMap.get(item.data.type);
const field = type && type.fields[item.data.fidx];
const method = type && type.methods[item.data.midx];
if (!type) {
return item;
}
let detail, documentation, header;
if (field) {
detail = field.label;
documentation = field.docs;
header = `${field.type.simpleTypeName} **${field.name}**`;
} else if (method) {
detail = `${method.modifiers.filter(m => !/abstract|transient|native/.test(m)).join(' ')} ${type.simpleTypeName}.${method.name}`;
documentation = method.docs;
header = method.shortlabel.replace(/^\w+/, x => `**${x}**`).replace(/^(.+?)\s*:\s*(.+)/, (_,a,b) => `${b} ${a}`);
} else {
detail = type.fullyDottedRawName,
documentation = type.docs,
header = `${type.typeKind} **${type.dottedTypeName}**`;
}
item.detail = detail || '';
item.documentation = formatDoc(header, documentation);
return item;
}
exports.getCompletionItems = getCompletionItems;
exports.resolveCompletionItem = resolveCompletionItem;
exports.clearDefaultCompletionEntries = clearDefaultCompletionEntries;

View File

@@ -0,0 +1,35 @@
/**
* Convert JavaDoc content to markdown used by vscode.
*
* This is a *very* rough conversion, simply looking for HTML tags and replacing them
* with relevant markdown characters.
* It is neither complete, nor perfect.
*
* @param {string} header
* @param {string} documentation
* @returns {import('vscode-languageserver').MarkupContent}
*/
function formatDoc(header, documentation) {
return {
kind: 'markdown',
value: `${header ? header + '\n\n' : ''}${
(documentation || '')
.replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '')
.replace(/(\n[ \t]*@[a-z]+)|(<p(?: .*)?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => {
return prm ? ` ${prm}`
: p ? '\n\n'
: i ? '*'
: b ? '**'
: tt ? '`'
: c ? '\n```'
: lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``)
: li ? '\n- '
: a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`)
: h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} `
: '';
})
}`,
};
}
exports.formatDoc = formatDoc;

283
langserver/document.js Normal file
View File

@@ -0,0 +1,283 @@
const { CEIType } = require('java-mti');
const ParseProblem = require('./java/parsetypes/parse-problem');
const { parse } = require('./java/body-parser');
const { SourceUnit } = require('./java/source-types');
const { parseMethodBodies } = require('./java/validater');
const { time, timeEnd, trace } = require('./logging');
/**
* Marker to prevent early parsing of source files before we've completed our
* initial source file load (we cannot accurately parse individual files until we
* know what all the types are - hence the need to perform a first parse of all the source files).
*
* While we are waiting for the first parse to complete, individual files-to-parse are added
* to this set. Once the first scan and parse is done, these are reparsed and
* first_parse_waiting is set to `null`.
* @type {Set<string>}
*/
let first_parse_waiting = new Set();
/**
* Convert a line,character position to an absolute character offset
*
* @param {{line:number,character:number}} pos
* @param {string} content
*/
function indexAt(pos, content) {
let idx = 0;
for (let i = 0; i < pos.line; i++) {
idx = content.indexOf('\n', idx) + 1;
if (idx === 0) {
return content.length;
}
}
return Math.min(idx + pos.character, content.length);
}
/**
* Convert an absolute character offset to a line,character position
*
* @param {number} index
* @param {string} content
*/
function positionAt(index, content) {
let line = 0,
last_nl_idx = 0,
character = 0;
if (index <= 0) return { line, character };
for (let idx = 0; ;) {
idx = content.indexOf('\n', idx) + 1;
if (idx === 0 || idx > index) {
if (idx === 0) index = content.length;
character = index - last_nl_idx;
return { line, character };
}
last_nl_idx = idx;
line++;
}
}
/**
* A specialised Map to allow for case-insensitive fileURIs on Windows.
*
* For cs-filesystems, this should work as a normal map.
* For ci-filesystems, if a file URI case changes, it should be picked up
* by the lowercase map
*/
class FileURIMap extends Map {
lowerMap = new Map();
/**
* @param {string} key
*/
get(key) {
return super.get(key) || this.lowerMap.get(key.toLowerCase());
}
/**
* @param {string} key
*/
has(key) {
return super.has(key) || this.lowerMap.has(key.toLowerCase());
}
/**
* @param {string} key
* @param {*} value
*/
set(key, value) {
super.set(key, value);
this.lowerMap.set(key.toLowerCase(), value);
return this;
}
/**
* @param {string} key
*/
delete(key) {
this.lowerMap.delete(key.toLowerCase());
return super.delete(key);
}
clear() {
super.clear();
this.lowerMap.clear();
}
}
/**
* Class for storing data about Java source files
*/
class JavaDocInfo {
/**
* @param {string} uri the file URI
* @param {string} content the full file content
* @param {number} version revision number for edited files (each edit increments the version)
*/
constructor(uri, content, version) {
this.uri = uri;
this.content = content;
this.version = version;
/**
* The result of the Java parse
* @type {ParsedInfo}
*/
this.parsed = null;
/**
* Promise linked to a timer which resolves a short time after the user stops typing
* - This is used to prevent constant reparsing while the user is typing in the document
* @type {Promise}
*/
this.reparseWaiter = Promise.resolve();
/** @type {{ resolve: () => void, timer: * }} */
this.waitInfo = null;
}
/**
* Schedule this document for reparsing.
*
* To prevent redundant parsing while typing, a small delay is required
* before the reparse happens.
* When a key is pressed, `scheduleReparse()` starts a timer. If more
* keys are typed before the timer expires, the timer is restarted.
* Once typing pauses, the timer expires and the content reparsed.
*
* A `reparseWaiter` promise is used to delay actions like completion items
* retrieval and method signature resolving until the reparse is complete.
*
* @param {Map<string,JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
*/
scheduleReparse(liveParsers, androidLibrary) {
const createWaitTimer = () => {
return setTimeout(() => {
// reparse the content, resolve the reparseWaiter promise
// and reset the fields
reparse([this.uri], liveParsers, androidLibrary, { includeMethods: true });
this.waitInfo.resolve();
this.waitInfo = null;
}, 250);
}
if (this.waitInfo) {
// we already have a promise pending - just restart the timer
trace('restart timer');
clearTimeout(this.waitInfo.timer);
this.waitInfo.timer = createWaitTimer();
return;
}
// create a new pending promise and start the timer
trace('start timer');
this.waitInfo = {
resolve: null,
timer: createWaitTimer(),
}
this.reparseWaiter = new Promise(resolve => this.waitInfo.resolve = resolve);
}
}
/**
* Result from parsing a Java file
*/
class ParsedInfo {
/**
* @param {string} uri the file URI
* @param {string} content the full file content
* @param {number} version the version this parse applies to
* @param {Map<string,CEIType>} typemap the set of known types
* @param {SourceUnit} unit the parsed unit
* @param {ParseProblem[]} problems
*/
constructor(uri, content, version, typemap, unit, problems) {
this.uri = uri;
this.content = content;
this.version = version;
this.typemap = typemap;
this.unit = unit;
this.problems = problems;
}
}
/**
* @param {string[]} uris
* @param {Map<string, JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
* @param {{includeMethods: boolean, first_parse?: boolean}} [opts]
*/
function reparse(uris, liveParsers, androidLibrary, opts) {
trace(`reparse`);
if (!Array.isArray(uris)) {
return;
}
if (first_parse_waiting) {
if (!opts || !opts.first_parse) {
// we are waiting for the first parse to complete - add this file to the list
uris.forEach(uri => first_parse_waiting.add(uri));
trace('waiting for first parse')
return;
}
}
if (androidLibrary instanceof Promise) {
// reparse after the library has finished loading
androidLibrary.then(lib => reparse(uris, liveParsers, lib, opts));
return;
}
const cached_units = [], parsers = [];
for (let docinfo of liveParsers.values()) {
if (uris.includes(docinfo.uri)) {
// make a copy of the content + version in case the source file is edited while we're parsing
parsers.push({uri: docinfo.uri, content: docinfo.content, version: docinfo.version});
} else if (docinfo.parsed) {
cached_units.push(docinfo.parsed.unit);
}
}
// Each parse uses a unique typemap, initialised from the android library
const typemap = new Map(androidLibrary);
// perform the parse
const units = parse(parsers, cached_units, typemap);
// create new ParsedInfo instances for each of the parsed units
units.forEach(unit => {
const parser = parsers.find(p => p.uri === unit.uri);
if (!parser) return;
const doc = liveParsers.get(unit.uri);
if (!doc) return;
doc.parsed = new ParsedInfo(doc.uri, parser.content, parser.version, typemap, unit, []);
});
let method_body_uris = [];
if (first_parse_waiting) {
// this is the first parse - parse the bodies of any waiting URIs and
// set first_parse_waiting to null
method_body_uris = [...first_parse_waiting];
first_parse_waiting = null;
}
if (opts && opts.includeMethods) {
method_body_uris = uris;
}
if (method_body_uris.length) {
time('parse-methods');
method_body_uris.forEach(uri => {
const doc = liveParsers.get(uri);
if (!doc || !doc.parsed) {
return;
}
parseMethodBodies(doc.parsed.unit, typemap);
})
timeEnd('parse-methods');
}
}
exports.indexAt = indexAt;
exports.positionAt = positionAt;
exports.FileURIMap = FileURIMap;
exports.JavaDocInfo = JavaDocInfo;
exports.ParsedInfo = ParsedInfo;
exports.reparse = reparse;

View 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
View 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;

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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,
}

View File

@@ -0,0 +1,72 @@
const fs = require('fs');
const path = require('path');
const { CEIType, loadJavaLibraryCacheFile } = require('java-mti');
const analytics = require('../analytics');
const { trace, time, timeEnd } = require('../logging');
/**
* @param {string} extensionPath install path of extension
* @param {string[]} additional_libs set of androidx library names to include eg: ["androidx.activity:activity"]
* @returns {Promise<Map<string,CEIType>>}
*/
async function loadAndroidSystemLibrary(extensionPath, additional_libs) {
analytics.time('android-library-load');
time('android-library-load');
let library;
try {
if (!extensionPath) {
throw new Error('Missing extension path')
}
const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
trace(`loading android library from ${cache_folder} with androidx libs: ${JSON.stringify(additional_libs)}`)
const typemap = await loadJavaLibraryCacheFile(path.join(cache_folder, 'android-29.zip'));
if (Array.isArray(additional_libs) && additional_libs.length) {
await loadJavaLibraryCacheFile(path.join(cache_folder, 'androidx-20200701.zip'), additional_libs, typemap);
}
trace(`loaded ${typemap.size} types into android library`);
library = typemap;
} finally {
timeEnd('android-library-load');
analytics.timeEnd('android-library-load', 'ms', { libs: additional_libs, typecount: library ? library.size : 0 });
}
return library;
}
/**
* @param {string} cache_folder
*/
async function loadHighestAPIPlatform(cache_folder) {
/** @type {fs.Dirent[]} */
const files = await new Promise((res, rej) => {
fs.readdir(cache_folder, {withFileTypes: true}, (err, files) => err ? rej(err) : res(files));
});
// find the file with the highest API level
let best_match = {
api: 0,
/** @type {fs.Dirent} */
file: null,
};
files.forEach(file => {
const m = file.name.match(/^android-(\d+)\.zip$/);
if (!m) return;
const api = parseInt(m[1], 10);
if (api > best_match.api) {
best_match = {
api,
file,
}
}
});
if (!best_match.file) {
throw new Error(`No valid platform cache files found in ${cache_folder}`)
}
console.log(`loading android platform cache: ${best_match.file.name}`);
const cache_file = path.join(cache_folder, best_match.file.name);
const typemap = loadJavaLibraryCacheFile(cache_file);
return typemap;
}
exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary;

View 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;

View 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;

View 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;

View 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,
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,6 @@
const { Statement } = require("./Statement");
class EmptyStatement extends Statement {
}
exports.EmptyStatement = EmptyStatement;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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,
}

View 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;

View 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,
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1,17 @@
const { SourceUnit } = require('../source-types');
const ParseProblem = require('../parsetypes/parse-problem');
/**
* @param {SourceUnit} unit
*/
module.exports = function(mod, unit) {
/** @type {ParseProblem[]} */
const probs = [];
unit.imports.forEach(i => {
if (!i.resolved)
probs.push(ParseProblem.Warning(i.nameTokens, `Unresolved import: ${i.package_name}`));
})
return probs;
}

60
langserver/logging.js Normal file
View 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;

View File

@@ -0,0 +1,92 @@
const { Method } = require('java-mti');
const { indexAt } = require('./document');
const { formatDoc } = require('./doc-formatter');
const { trace } = require('./logging');
const { event } = require('./analytics');
let methodsigRequestCount = 0;
/**
* Retrieve method signature information
*
* Each parsed token that is relevant to a method call is
* tagged with the list of possible methods and the best matched
* method. The tagged tokens include:
* - the opening bracket
* - each token in every argument
* - each comma between the arguments
*
* The function locates the nearest non-ws token and checks
* for any tagged method-call info. It then converts it
* to the relevant vscode method signature structure for display.
*
* @param {import('vscode-languageserver').SignatureHelpParams} request
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
*/
async function getSignatureHelp(request, liveParsers) {
trace('getSignatureHelp');
/** @type {import('vscode-languageserver').SignatureHelp} */
let sighelp = {
signatures: [],
activeSignature: 0,
activeParameter: 0,
}
const docinfo = liveParsers.get(request.textDocument.uri);
if (!docinfo || !docinfo.parsed) {
return sighelp;
}
// wait for any active edits to complete
await docinfo.reparseWaiter;
methodsigRequestCount += 1;
if ((methodsigRequestCount === 1) || (methodsigRequestCount === 5) || ((methodsigRequestCount % 25) === 0)) {
event('method-sig-requests', { methsig_req_count: methodsigRequestCount });
}
// locate the token at the requested position
const index = indexAt(request.position, docinfo.content);
const token = docinfo.parsed.unit.getTokenAt(index);
if (!token || !token.methodCallInfo) {
trace('onSignatureHelp - no method call info');
return sighelp;
}
// the token has method information attached to it
// - convert it to the required vscode format
trace(`onSignatureHelp - ${token.methodCallInfo.methods.length} methods`);
sighelp = {
signatures: token.methodCallInfo.methods.map(m => {
const documentation = formatDoc(`#### ${m.owner.simpleTypeName}${m instanceof Method ? `.${m.name}` : ''}()`, m.docs);
const param_docs = new Map();
if (documentation) {
// extract each of the @param sections (if any)
for (let m, re=/@param\s+(\S+)([\d\D]+?)(?=\n\n|\n[ \t*]*@\w+|$)/g; m = re.exec(documentation.value);) {
param_docs.set(m[1], m[2]);
}
}
/** @type {import('vscode-languageserver').SignatureInformation} */
let si = {
label: m.label,
documentation,
parameters: m.parameters.map(p => {
/** @type {import('vscode-languageserver').ParameterInformation} */
let pi = {
documentation: {
kind: 'markdown',
value: param_docs.has(p.name) ? `**${p.name}**: ${param_docs.get(p.name)}` : '',
},
label: p.label
}
return pi;
})
}
return si;
}),
activeSignature: token.methodCallInfo.methodIdx,
activeParameter: token.methodCallInfo.argIdx,
}
return sighelp;
}
exports.getSignatureHelp = getSignatureHelp;

352
langserver/package-lock.json generated Normal file
View File

@@ -0,0 +1,352 @@
{
"name": "langserver",
"version": "1.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/node": {
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
},
"binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
"requires": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
}
},
"bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-indexof-polyfill": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8="
},
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
},
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
"requires": {
"traverse": ">=0.3.0 <0.4"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
"requires": {
"readable-stream": "^2.0.2"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
},
"https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"java-mti": {
"version": "github:adelphes/java-mti#ec164ace68267813a1ca9df18651b51e3f3f067d",
"from": "github: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": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mixpanel": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.11.0.tgz",
"integrity": "sha512-TS7AkCmfC+vGshlCOjEcITFoFxlt5fdSEqmN+d+pTXAhE5v+jPQW2uUcn9W+Oq4NVXz+kdskU09dsm9vmNl0ig==",
"requires": {
"https-proxy-agent": "3.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
},
"unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"requires": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageserver": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
"requires": {
"vscode-languageserver-protocol": "^3.15.3"
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-uri": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz",
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

22
langserver/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "langserver",
"version": "1.0.3",
"description": "Language server for Android development",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"java-mti": "adelphes/java-mti#ec164ac",
"mixpanel": "0.11.0",
"uuid": "8.2.0",
"vscode-languageserver": "6.1.1",
"vscode-languageserver-textdocument": "1.0.1",
"vscode-uri": "2.1.2"
},
"devDependencies": {
"@types/node": "^13.13.4"
}
}

300
langserver/server.js Normal file
View File

@@ -0,0 +1,300 @@
const {
createConnection,
TextDocuments,
ProposedFeatures,
DidChangeConfigurationNotification,
TextDocumentSyncKind,
} = require('vscode-languageserver');
const fs = require('fs');
const { TextDocument } = require('vscode-languageserver-textdocument');
const { URI } = require('vscode-uri');
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
const { CEIType } = require('java-mti');
const { Settings } = require('./settings');
const { trace } = require('./logging');
const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem } = require('./completions');
const { getSignatureHelp } = require('./method-signatures');
const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document');
const { v4: uuidv4 } = require('uuid');
const analytics = require('./analytics');
const package_json = require('./package.json');
/**
* The global map of Android system types
* @typedef {Map<string, CEIType>} AndroidLibrary
* @type {AndroidLibrary|Promise<AndroidLibrary>}
*/
let androidLibrary = null;
/**
* The list of loaded Java documents
* @type {Map<string,JavaDocInfo>}
*/
const liveParsers = new FileURIMap();
let startupOpts = null;
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
function loadCodeCompletionLibrary(extensionPath, codeCompletionLibraries) {
// the android library is loaded asynchronously, with the global `androidLibrary` variable
// set to the promise while it is loading.
androidLibrary = (androidLibrary instanceof Promise
? androidLibrary // if we're currently loading, wait for it to complete
: Promise.resolve(new Map())
)
.then(() => loadAndroidSystemLibrary(extensionPath, codeCompletionLibraries))
.then(
library => androidLibrary = library,
err => {
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
return new Map();
}
);
}
// Text document manager monitoring file opens and edits
let documents = new TextDocuments({
/**
*
* @param {string} uri
* @param {string} languageId
* @param {number} version
* @param {string} content
*/
create(uri, languageId, version, content) {
trace(`document create ${uri}:${version}`);
// sanity-check - we only support Java source files
if (!/\.java$/i.test(uri)) {
return { uri };
}
// add the document to the set
liveParsers.set(uri, new JavaDocInfo(uri, content, version));
// tokenize the file content and build the initial parse state
reparse([uri], liveParsers, androidLibrary, { includeMethods: true });
return { uri };
},
/**
*
* @param {TextDocument} document
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
* @param {number} version
*/
update(document, changes, version) {
trace(`document update ${document.uri}:${version}`);
if (!liveParsers.has(document.uri)) {
return;
}
const docinfo = liveParsers.get(document.uri);
if (!docinfo) {
return;
}
// apply the edits to our local content copy
changes.forEach((change) => {
/** @type {import('vscode-languageserver').Range} */
const r = change['range'];
if (r) {
const start_index = indexAt(r.start, docinfo.content);
let end_index = start_index + (r.end.character - r.start.character);
if (r.end.line !== r.start.line) end_index = indexAt(r.end, docinfo.content);
docinfo.content = `${docinfo.content.slice(0, start_index)}${change.text}${docinfo.content.slice(end_index)}`;
}
});
docinfo.version = version;
docinfo.scheduleReparse(liveParsers, androidLibrary);
return document;
},
});
// Create a connection for the server. The connection uses Node's IPC as a transport.
const connection = createConnection(ProposedFeatures.all);
connection.onInitialize((params) => {
startupOpts = {
extensionPath: '',
initialSettings: {
appSourceRoot: '',
/** @type {string[]} */
codeCompletionLibraries: [],
trace: false,
},
sourceFiles: [],
...params.initializationOptions,
}
Settings.set(startupOpts.initialSettings);
analytics.init(undefined, startupOpts.mpuid, uuidv4(), package_json, { vscode_version: startupOpts.vscodeVersion });
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
let capabilities = params.capabilities;
// Does the client support the `workspace/configuration` request?
// If not, we will fall back using global settings
hasConfigurationCapability = capabilities.workspace && !!capabilities.workspace.configuration;
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
/** @type {string[]} */
const file_uris = Array.isArray(startupOpts.sourceFiles) ? startupOpts.sourceFiles : [];
for (let file_uri of file_uris) {
const file = URI.parse(file_uri, true);
const filePath = file.fsPath;
if (!/.java/i.test(filePath)) {
trace(`ignoring non-java file: ${filePath}`);
continue;
}
if (liveParsers.has(file_uri)) {
trace(`File already loaded: ${file_uri}`);
continue;
}
try {
// it's fine to load the initial file set synchronously - the language server runs in a
// separate process and nothing (useful) can happen until the first parse is complete.
const content = fs.readFileSync(file.fsPath, 'utf8');
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
trace(`Added initial file: ${file_uri}`);
} catch (err) {
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
}
}
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
return {
capabilities: {
// we support incremental updates
textDocumentSync: TextDocumentSyncKind.Incremental,
// we support code completion
completionProvider: {
resolveProvider: true,
},
// we support method signature information
signatureHelpProvider : {
triggerCharacters: [ '(' ]
}
},
};
});
connection.onInitialized(async () => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(
DidChangeConfigurationNotification.type, {
section: 'android-dev-ext',
});
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
trace('Workspace folder change event received.');
});
}
trace('Initialization complete');
});
connection.onDidChangeConfiguration(async (change) => {
trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`);
const prev_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
// fetch and update the settings
const newSettings = await connection.workspace.getConfiguration({
section: "android-dev-ext"
});
Settings.set(newSettings);
if (Settings.updateCount > 2) {
analytics.event('ls-settings-changed', {
appSourceRoot: Settings.appSourceRoot,
libs: Settings.codeCompletionLibraries,
trace: Settings.trace,
})
}
const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) {
// code-completion libraries have changed - reload the android library
trace("code completion libraries changed - reloading android library and reparsing")
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false });
clearDefaultCompletionEntries();
}
})
documents.onDidClose((e) => {
trace(`doc closed ${e.document.uri}`);
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
});
connection.onDidChangeWatchedFiles(
/** @param {import('vscode-languageserver').DidChangeWatchedFilesParams} params */
(params) => {
// Monitored files have change in VS Code
trace(`watch file change: ${JSON.stringify(params)}`);
let files_changed = false;
params.changes.forEach(change => {
switch(change.type) {
case 1: // create
// if the user creates the file directly in vscode, the file will automatically open (and we receive an open callback)
// - but if the user creates or copies a file into the workspace, we need to manually add it to the set.
if (!liveParsers.has(change.uri)) {
trace(`file added: ${change.uri}`)
try {
const fname = URI.parse(change.uri, true).fsPath;
liveParsers.set(change.uri, new JavaDocInfo(change.uri, fs.readFileSync(fname, 'utf8'), 0));
files_changed = true;
} catch (err) {
console.log(`Failed to add new file '${change.uri}' to working set. ${err.message}`);
}
}
break;
case 2: // change
// called when the user manually saves the file - ignore for now
break;
case 3: // delete
trace(`file deleted: ${change.uri}`)
liveParsers.delete(change.uri);
files_changed = true;
break;
}
});
if (files_changed) {
// reparse the entire set
reparse([...liveParsers.keys()], liveParsers, androidLibrary);
}
}
);
// Retrieve the initial list of the completion items.
connection.onCompletion(params => getCompletionItems(params, liveParsers, androidLibrary));
// Resolve additional information for the item selected in the completion list.
connection.onCompletionResolve(item => resolveCompletionItem(item));
// Retrieve method signature information
connection.onSignatureHelp(params => getSignatureHelp(params, liveParsers));
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();

42
langserver/settings.js Normal file
View 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;

View 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();

67
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "android-dev-ext",
"version": "1.0.0",
"version": "1.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -37,15 +37,15 @@
"dev": true
},
"@types/node": {
"version": "10.17.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.20.tgz",
"integrity": "sha512-XgDgo6W10SeGEAM0k7FosJpvLCynOTYns4Xk3J5HGrA+UI/bKZ30PGMzOP5Lh2zs4259I71FSYLAtjnx3qhObw==",
"version": "10.17.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.21.tgz",
"integrity": "sha512-PQKsydPxYxF1DsAFWmunaxd3sOi3iMt6Zmx/tgaagHYmwJ/9cRH91hQkeJZaUGWbvn0K5HlSVEXkn5U/llWPpQ==",
"dev": true
},
"@types/vscode": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.24.0.tgz",
"integrity": "sha512-Zu4LCYJXkMcae01zGf8IFcMeDHZo509MgK/OW7qo/Zw/ZoeYSHpchrsSVPHrXxNeFC5CPAZ210c7tM7EnImz+w==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.46.0.tgz",
"integrity": "sha512-8m9wPEB2mcRqTWNKs9A9Eqs8DrQZt0qNFO8GkxBOnyW6xR//3s77SoMgb/nY1ctzACsZXwZj3YRTDsn4bAoaUw==",
"dev": true
},
"acorn": {
@@ -1183,9 +1183,9 @@
"dev": true
},
"unzipper": {
"version": "0.10.10",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz",
"integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==",
"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",
@@ -1214,9 +1214,9 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
},
"vscode-debugadapter": {
"version": "1.40.0",
@@ -1232,6 +1232,41 @@
"resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0.tgz",
"integrity": "sha512-Fwze+9qbLDPuQUhtITJSu/Vk6zIuakNM1iR2ZiZRgRaMEgBpMs2JSKaT0chrhJHCOy6/UbpsUbUBIseF6msV+g=="
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageclient": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz",
"integrity": "sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==",
"requires": {
"semver": "^6.3.0",
"vscode-languageserver-protocol": "^3.15.3"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -1262,9 +1297,9 @@
}
},
"ws": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
"integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
},
"xmldom": {
"version": "0.1.31",

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "1.0.0",
"version": "1.3.1",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -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,6 +282,11 @@
"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.",
@@ -101,6 +309,11 @@
"-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\"",
@@ -109,7 +322,50 @@
"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
}
}
},
"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",
@@ -122,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": {}
@@ -148,22 +424,24 @@
]
},
"scripts": {
"postinstall": "cd langserver && npm install && cd ..",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"long": "^4.0.0",
"unzipper": "^0.10.10",
"uuid": "^3.3.2",
"vscode-debugadapter": "^1.40.0",
"vscode-debugprotocol": "^1.40.0",
"ws": "^7.1.2",
"xmldom": "^0.1.27",
"xpath": "^0.0.27"
"long": "4.0.0",
"unzipper": "0.10.11",
"uuid": "8.2.0",
"vscode-debugadapter": "1.40.0",
"vscode-debugprotocol": "1.40.0",
"vscode-languageclient": "6.1.3",
"ws": "7.3.0",
"xmldom": "0.1.31",
"xpath": "0.0.27"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.5",
"@types/vscode": "1.24.0",
"@types/vscode": "^1.24.0",
"eslint": "^5.9.0",
"mocha": "^5.2.0",
"typescript": "^3.8.3"

View File

@@ -36,17 +36,94 @@ function parse_device_list(data, extended = false) {
return devicelist;
}
let adbSocketParams;
/**
* Return the host and port for connecting to the ADB server
*/
function getADBSocketParams() {
// this is memoized to prevent alterations once the debug session is up and running
if (adbSocketParams) {
return adbSocketParams;
}
return adbSocketParams = getIntialADBSocketParams();
}
/**
* Retrieve the socket parameters for connecting to an ADB server instance.
* In priority order (highest first):
* 1. adbSocket debug configuration value
* 2. non-default adbPort debug configuration value (using localhost)
* 3. ADB_SERVER_SOCKET environment variable
* 4. ANDROID_ADB_SERVER_ADDRESS / ANDROID_ADB_SERVER_PORT environment variables
* 5. [localhost]:5037
*/
function getIntialADBSocketParams() {
/**
* Retrieve a trimmed environment variable or return a blank string
* @param {string} name
*/
function envValue(name) {
return (process.env[name] || '').trim();
}
function decode_port_string(s) {
if (!/^\d+$/.test(s)) {
return;
}
const portnum = parseInt(s, 10);
if (portnum < 1 || portnum > 65535) {
return;
}
return portnum;
}
const default_host = '', default_port = 5037;
// the ADBSocket.HostPort value is automatically set with adbSocket/adbPort values from
// the debug configuration when the debugger session starts.
let socket_str = ADBSocket.HostPort.trim();
if (socket_str !== ADBSocket.DefaultHostPort) {
// non-default debug configuration values are configured (1. or 2.)
const [host, port] = socket_str.split(':');
return {
host,
port: decode_port_string(port) || default_port
}
}
// ADB_SERVER_SOCKET=tcp:<host>:<port>
const adb_server_socket_match = envValue('ADB_SERVER_SOCKET').match(/^\s*tcp(?::(.*))?(?::(\d+))\s*$/);
if (adb_server_socket_match) {
return {
host: adb_server_socket_match[1] || default_host,
port: decode_port_string(adb_server_socket_match[2]) || default_port,
}
}
return {
host: envValue('ANDROID_ADB_SERVER_ADDRESS') || default_host,
port: decode_port_string(envValue('ANDROID_ADB_SERVER_PORT')) || default_port,
}
}
class ADBClient {
/**
* @param {string} [deviceid]
* @param {number} [adbPort] the port number to connect to ADB
* @param {number} [adbHost] the hostname/ip address to connect to ADB
*/
constructor(deviceid, adbPort = ADBSocket.ADBPort) {
constructor(deviceid, adbPort, adbHost) {
this.deviceid = deviceid;
this.adbsocket = null;
this.jdwp_socket = null;
this.adbPort = adbPort;
const default_adb_socket = getADBSocketParams();
this.adbHost = adbHost || default_adb_socket.host;
this.adbPort = adbPort || default_adb_socket.port;
}
async test_adb_connection() {
@@ -68,14 +145,77 @@ class ADBClient {
}
/**
* Return a list of debuggable pids from the device
* Return a list of debuggable pids from the device.
*
* The `adb jdwp` command never terminates - it just posts each debuggable PID
* as it comes online. Normally we just perform a single read of stdout
* and terminate the connection, but if there are no pids available, the command
* will wait forever.
* @param {number} [timeout_ms] time to wait before we abort reading (and return an empty list).
*/
async jdwp_list() {
async jdwp_list(timeout_ms) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
const stdout = await this.adbsocket.cmd_and_read_stdout('jdwp');
/** @type {string} */
let stdout;
try {
stdout = await this.adbsocket.cmd_and_read_stdout('jdwp', timeout_ms);
} catch {
// timeout or socket closed
stdout = '';
}
await this.disconnect_from_adb();
return stdout.trim().split(/\r?\n|\r/);
// do not sort the pid list - the debugger needs to pick the last one in the list.
return stdout.trim().split(/\s+/).filter(x => x).map(s => parseInt(s, 10));
}
/**
* Retrieve a list of named debuggable pids
* @param {number} timeout_ms
*/
async named_jdwp_list(timeout_ms) {
const pids = await this.jdwp_list(timeout_ms);
return this.get_named_processes(pids);
}
/**
* Convert a list of pids to named-process objects
* @param {number[]} pids
*/
async get_named_processes(pids) {
if (!pids.length) {
return [];
}
const named_pids = pids
.map(pid => ({
pid,
name: '',
}))
// retrieve the list of process names from the device
const command = `for pid in ${pids.join(' ')}; do cat /proc/$pid/cmdline;echo " $pid"; done`;
const stdout = await this.shell_cmd({
command,
untilclosed: true,
});
// output should look something like...
// com.example.somepkg 32721
const lines = stdout.replace(/\0+/g,'').split(/\r?\n|\r/g);
// scan the list looking for pids to match names with...
for (let i = 0; i < lines.length; i++) {
let entries = lines[i].match(/^\s*(.*)\s+(\d+)$/);
if (!entries) {
continue;
}
const pid = parseInt(entries[2], 10);
const named_pid = named_pids.find(x => x.pid === pid);
if (named_pid) {
named_pid.name = entries[1];
}
}
return named_pids;
}
/**
@@ -107,7 +247,10 @@ class ADBClient {
// note that upon success, this method does not close the connection (it must be left open for
// future commands to be sent over the jdwp socket)
this.jdwp_socket = new JDWPSocket(o.onreply, o.ondisconnect);
await this.jdwp_socket.connect(o.localport)
// assume the 'local' port (routed to connect to the process on the device)
// is set up on the same host that the adb server is running on
const adb_server_socket = getADBSocketParams();
await this.jdwp_socket.connect(o.localport, adb_server_socket.host);
await this.jdwp_socket.start();
return true;
}
@@ -132,12 +275,14 @@ class ADBClient {
/**
* Run a shell command on the connected device
* @param {{command:string}} o
* @param {{command:string, untilclosed?:boolean}} o
* @param {number} [timeout_ms]
* @returns {Promise<string>}
*/
async shell_cmd(o) {
async shell_cmd(o, timeout_ms) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`);
const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`, timeout_ms, o.untilclosed);
await this.disconnect_from_adb();
return stdout;
}
@@ -145,7 +290,7 @@ class ADBClient {
/**
* Starts the Logcat monitor.
* Logcat lines are passed back via onlog callback. If the device disconnects, onclose is called.
* @param {{onlog:(e)=>void, onclose:()=>void}} o
* @param {{onlog:(e)=>void, onclose:(err)=>void}} o
*/
async startLogcatMonitor(o) {
// onlog:function(e)
@@ -157,18 +302,19 @@ class ADBClient {
if (!o.onlog) {
const logcatbuffer = await this.adbsocket.read_stdout();
await this.disconnect_from_adb();
return logcatbuffer;
return logcatbuffer.toString();
}
// start the logcat monitor
let logcatbuffer = Buffer.alloc(0);
const next_logcat_lines = async () => {
// read the next data from ADB
let logcatbuffer = Buffer.alloc(0);
let next_data;
for (;;) {
// read the next data from ADB
try {
next_data = await this.adbsocket.read_stdout(null);
next_data = await this.adbsocket.read_stdout();
} catch(e) {
o.onclose();
o.onclose(e);
return;
}
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
@@ -178,16 +324,19 @@ class ADBClient {
next_logcat_lines();
return;
}
// split into lines
const logs = logcatbuffer.slice(0, last_newline_index).toString().split(/\r\n?|\n/);
logcatbuffer = logcatbuffer.slice(last_newline_index);
// split into lines, sort and remove duplicates and blanks
const logs = logcatbuffer.slice(0, last_newline_index).toString()
.split(/\r\n?|\n/)
.sort()
.filter((line,idx,arr) => line && line !== arr[idx-1]);
logcatbuffer = logcatbuffer.slice(last_newline_index);
const e = {
adbclient: this,
logs,
};
o.onlog(e);
next_logcat_lines();
}
}
next_logcat_lines();
}
@@ -207,12 +356,9 @@ class ADBClient {
return true;
}
/**
* @param {string} [hostname]
*/
connect_to_adb(hostname = '127.0.0.1') {
connect_to_adb() {
this.adbsocket = new ADBSocket();
return this.adbsocket.connect(this.adbPort, hostname);
return this.adbsocket.connect(this.adbPort, this.adbHost);
}
disconnect_from_adb () {
@@ -221,3 +367,4 @@ class ADBClient {
};
exports.ADBClient = ADBClient;
exports.getADBSocketParams = getADBSocketParams;

View File

@@ -107,7 +107,7 @@ function decode_spec_value(o, key, value, buf, idx, main) {
case /^align:\d+$/.test(value): {
// used for arbitrary padding to a specified alignment
const align = parseInt(value.split(':')[1], 10);
byteLength = align - (idx % align);
byteLength = idx - (Math.trunc(idx / align) * align);
o[key] = buf.slice(idx, idx + byteLength);
break;
}

View File

@@ -17,6 +17,11 @@ class APKFileInfo {
*/
file_data = null;
/**
* The size of the APK file (in bytes)
*/
file_size = 0;
/**
* last modified time of the APK file (in ms)
*/
@@ -67,6 +72,7 @@ class APKFileInfo {
// read the APK file contents
try {
result.file_data = await readFile(args.apkFile);
result.file_size = result.file_data.length;
} catch(err) {
throw new Error(`APK read error. ${err.message}`);
}
@@ -104,8 +110,7 @@ class APKFileInfo {
* 2. The decoded manifest from the APK
* 3. The AndroidManifest.xml file from the root of the source tree.
*/
async function getAndroidManifestXml(args) {
const {manifestFile, apkFile, appSrcRoot} = args;
async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) {
let manifest;
// a value from the manifestFile overrides the default manifest extraction
@@ -121,7 +126,7 @@ async function getAndroidManifestXml(args) {
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}`);
D(`Reading source manifest from ${appSrcRoot} (${err.message})`);
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
}
return manifest;

View File

@@ -4,22 +4,24 @@ const {
Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
// node and external modules
const fs = require('fs');
const os = require('os');
const path = require('path');
const uuidv4 = require('uuid').v4;
// our stuff
const { ADBClient } = require('./adbclient');
const { APKFileInfo } = require('./apk-file-info');
const { Debugger } = require('./debugger');
const { BreakpointOptions, BuildInfo, DebuggerException, DebuggerValue, JavaBreakpointEvent, JavaClassType, JavaExceptionEvent, SourceLocation } = require('./debugger-types');
const { AttachBuildInfo, BreakpointOptions, DebuggerException, DebuggerValue, JavaBreakpointEvent, JavaClassType, JavaExceptionEvent, LaunchBuildInfo, SourceLocation } = require('./debugger-types');
const { evaluate } = require('./expression/evaluate');
const { PackageInfo } = require('./package-searcher');
const ADBSocket = require('./sockets/adbsocket');
const { AndroidThread } = require('./threads');
const { D, onMessagePrint } = require('./utils/print');
const { checkADBStarted, getAndroidSourcesFolder } = require('./utils/android');
const { D, initLogToClient, onMessagePrint } = require('./utils/print');
const { hasValidSourceFileExtension } = require('./utils/source-file');
const { VariableManager } = require('./variable-manager');
const analytics = require('../langserver/analytics');
class AndroidDebugSession extends DebugSession {
@@ -33,13 +35,6 @@ class AndroidDebugSession extends DebugSession {
// the base folder of the app (where AndroidManifest.xml and source files should be)
this.app_src_root = '<no appSrcRoot>';
// the filepathname of the built apk
this.apk_fpn = '';
/**
* the file info, hash and manifest data of the apk
* @type {APKFileInfo}
*/
this.apk_file_info = null;
// packages we found in the source tree
this.src_packages = {
last_src_modified: 0,
@@ -50,8 +45,29 @@ class AndroidDebugSession extends DebugSession {
this._device = null;
// the API level of the device we are debugging
this.device_api_level = '';
// the full file path name of the AndroidManifest.xml, taken from the manifestFile launch property
this.manifest_fpn = '';
// the filepathname of the built apk
this.apk_fpn = '';
/**
* the file info, hash and manifest data of the apk
* @type {APKFileInfo}
*/
this.apk_file_info = null;
/**
* array of custom arguments to pass to `pm install`
* @type {string[]}
*/
this.pm_install_args = null;
/**
* array of custom arguments to pass to `am start`
* @type {string[]}
*/
this.am_start_args = null;
/**
* the threads (from the last refreshThreads() call)
@@ -63,9 +79,12 @@ class AndroidDebugSession extends DebugSession {
this._android_sources_path = '';
// number of call stack entries to display above the project source
this.callStackDisplaySize = 1;
this.callStackDisplaySize = 0;
// the fifo queue of evaluations (watches, hover, etc)
/**
* the fifo queue of evaluations (watches, hover, etc)
* @type {EvalQueueEntry[]}
*/
this._evals_queue = [];
// since we want to send breakpoint events, we will assign an id to every event
@@ -82,34 +101,43 @@ class AndroidDebugSession extends DebugSession {
// trace flag for printing diagnostic messages to the client Output Window
this.trace = false;
// set to true if we've connected to the device
this.debuggerAttached = false;
/**
* @type {'launch'|'attach'}
*/
this.debug_mode = null;
this.terminate_reason = '';
this.session_id = uuidv4();
this.session_start = new Date();
analytics.init();
// this debugger uses one-based lines and columns
this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true);
// override the log function to output to the client Debug Console
initLogToClient(this.LOG.bind(this));
}
/**
* The 'initialize' request is the first request called by the frontend
* to interrogate the features the debug adapter provides.
* @param {import('vscode-debugprotocol').DebugProtocol.InitializeResponse} response
*/
initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
// This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true;
// we support some exception options
initializeRequest(response) {
response.body.exceptionBreakpointFilters = [
{ label:'All Exceptions', filter:'all', default:false },
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
];
// we support modifying variable values
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsSetVariable = true;
// we support hit-count conditional breakpoints
response.body.supportsHitConditionalBreakpoints = true;
// we support the new ExceptionInfoRequest
response.body.supportsEvaluateForHovers = true;
response.body.supportsExceptionInfoRequest = true;
response.body.supportsHitConditionalBreakpoints = true;
this.sendResponse(response);
}
@@ -139,6 +167,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} msg
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
* @param {boolean} silent
*/
failRequest(msg, response, silent = false) {
@@ -157,7 +186,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
failRequestNoThread(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response);
@@ -166,7 +195,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
failRequestThreadNotSuspended(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
@@ -175,7 +204,7 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} requestName
* @param {number} threadId
* @param {*} response
* @param {import('vscode-debugprotocol').DebugProtocol.Response} response
*/
cancelRequestThreadNotSuspended(requestName, threadId, response) {
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
@@ -230,10 +259,11 @@ class AndroidDebugSession extends DebugSession {
// configure the thread names
threadinfos.forEach(threadinfo => {
const thread = this.getThread(threadinfo.threadid);
if (thread.name === null) {
if (typeof thread.name !== 'string') {
thread.name = threadinfo.name;
} else if (thread.name !== threadinfo.name) {
// give the thread a new id for VS code
// - note: this will invalidate all current variable references for this thread
delete this._threads[thread.vscode_threadid];
thread.allocateNewThreadID();
this._threads[thread.vscode_threadid] = thread;
@@ -252,24 +282,263 @@ class AndroidDebugSession extends DebugSession {
})
}
async launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
/**
* @param {*} obj
*/
extractPidAndTargetDevice(obj) {
let x, pid, serial = '', status;
try {
x = JSON.parse(`${obj}`);
} catch {
}
if (typeof x === 'number') {
pid = x;
} else if (typeof x === 'object') {
// object passed from PickAndroidProcess in the extension
({ pid, serial, status } = x);
if (status !== 'ok') {
return null;
}
}
if (typeof pid !== "number" || (pid < 0)) {
this.LOG(`Attach failed: "processId" property in launch.json is not valid`);
return null;
}
return {
processId: pid,
targetDevice: `${serial}`,
}
}
extractTargetDeviceID(s) {
if (!s || typeof s !== 'string') {
return '';
}
// the device picker returns a stringified object
try {
const o = JSON.parse(s);
return o.serial || s;
} catch {
}
return s;
}
/**
* @typedef AndroidAttachArguments
* @property {number} adbPort
* @property {string} adbSocket
* @property {string} appSrcRoot
* @property {boolean} autoStartADB
* @property {number} jdwpPort
* @property {number} processId
* @property {string} targetDevice
* @property {boolean} trace
*
* @param {import('vscode-debugprotocol').DebugProtocol.AttachResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.AttachRequestArguments & AndroidAttachArguments} args
*/
async attachRequest(response, args) {
this.debug_mode = 'attach';
if (args && args.trace) {
this.trace = args.trace;
onMessagePrint(this.LOG.bind(this));
}
D(JSON.stringify({type: 'attach', args, env:process.env}, null, ' '));
if (args.targetDevice === 'null') {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.terminate_reason = "null-targetdevice";
this.sendEvent(new TerminatedEvent(false));
return;
}
if (!args.processId) {
this.LOG(`Attach failed: Missing "processId" property in launch.json`);
this.terminate_reason = "no-processid";
this.sendEvent(new TerminatedEvent(false));
return;
}
// the processId passed in args can be:
// - a fixed id defined in launch.json (should be a string, but we allow a number),
// - a JSON object returned from the process picker (contains the target device and process ID),
let attach_info = this.extractPidAndTargetDevice(args.processId);
if (!attach_info) {
this.terminate_reason = "null-attachinfo";
this.sendEvent(new TerminatedEvent(false));
return;
}
// set the custom ADB host and port
if (typeof args.adbSocket === 'string' && args.adbSocket) {
ADBSocket.HostPort = args.adbSocket;
} else if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) {
ADBSocket.HostPort = `:${args.adbPort}`;
}
// set the fixed JDWP port number (if any)
if (typeof args.jdwpPort === 'number' && args.jdwpPort >= 0 && args.jdwpPort <= 65535) {
Debugger.portManager.fixedport = args.jdwpPort;
}
try {
// app_src_root must end in a path-separator for correct validation of sub-paths
this.app_src_root = ensure_path_end_slash(args.appSrcRoot);
// start by scanning the source folder for stuff we need to know about (packages, manifest, etc)
this.src_packages = PackageInfo.scanSourceSync(this.app_src_root);
// warn if we couldn't find any packages (-> no source -> cannot debug anything)
if (this.src_packages.packages.size === 0)
this.WARN('No source files found. Check the "appSrcRoot" setting in launch.json');
} catch(err) {
// wow, we really didn't make it very far...
this.LOG(err.message);
this.LOG('Check the "appSrcRoot" entries in launch.json');
this.terminate_reason = `init-exception: ${err.message}`;
this.sendEvent(new TerminatedEvent(false));
return;
}
try {
let { processId, targetDevice } = attach_info;
if (!targetDevice) {
targetDevice = this.extractTargetDeviceID(args.targetDevice);
}
// make sure ADB exists and is started and look for a connected device
await checkADBStarted(args.autoStartADB !== false);
this._device = await this.findSuitableDevice(targetDevice, args.trace);
this._device.adbclient = new ADBClient(this._device.serial);
// try and determine the relevant path for the API sources (based upon the API level of the connected device)
await this.configureAPISourcePath();
const build = new AttachBuildInfo(new Map(this.src_packages.packages));
this.LOG(`Attaching to pid ${processId} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
// try and attach to the specified pid
await this.dbgr.attachToProcess(build, processId, this._device.serial);
this.debuggerAttached = true;
// if we get this far, the debugger is connected and waiting for the resume command
// - set up some events...
this.dbgr.on('bpstatechange', e => this.onBreakpointStateChange(e))
.on('bphit', e => this.onBreakpointHit(e))
.on('step', e => this.onStep(e))
.on('exception', e => this.onException(e))
.on('threadchange', e => this.onThreadChange(e))
.on('disconnect', () => this.onDebuggerDisconnect());
// - tell the client we're initialised and ready for breakpoint info, etc
this.sendEvent(new InitializedEvent());
await new Promise(resolve => this.waitForConfigurationDone = resolve);
// get the debugger to tell us about any thread creations/terminations
await this.dbgr.setThreadNotify();
// config is done - we're all set and ready to go!
this.sendResponse(response);
this.LOG(`Debugger attached`);
await this.dbgr.resume();
analytics.event('debug-started', {
dbg_session_id: this.session_id,
dbg_start: this.session_start.toLocaleTimeString(),
dbg_tz: this.session_start.getTimezoneOffset(),
dbg_kind: 'attach',
dbg_device_api: this.device_api_level,
dbg_emulator: /^emulator/.test(this._device.serial),
})
} catch(e) {
const msg = e.message||e.msg;
//this.performDisconnect();
// exceptions use message, adbclient uses msg
this.LOG('Attach failed: '+(msg||'No additional information is available'));
// more info for adb connect errors
if (/^ADB server is not running/.test(e.msg)) {
this.LOG('Make sure the Android SDK Platform Tools are installed and run:');
this.LOG(' adb start-server');
this.LOG('If you are running ADB using a non-default configuration, also make sure the adbSocket value in your launch.json is correct.');
}
if (/ADB|JDWP/.test(msg)) {
this.LOG('Ensure any instances of Android Studio are closed and ADB is running.');
}
// tell the client we're done
this.terminate_reason = `start-exception: ${msg}`;
this.sendEvent(new TerminatedEvent(false));
}
}
/**
* @typedef AndroidLaunchArguments
* @property {number} adbPort
* @property {string} adbSocket
* @property {string[]} amStartArgs
* @property {string} apkFile
* @property {string} appSrcRoot
* @property {boolean} autoStartADB
* @property {number} callStackDisplaySize
* @property {number} jdwpPort
* @property {string} launchActivity
* @property {string} manifestFile
* @property {string[]} pmInstallArgs
* @property {number} postLaunchPause
* @property {number} processId
* @property {StaleBuildSetting} staleBuild
* @property {string} targetDevice
* @property {boolean} trace
* The entry point to the debugger
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.LaunchRequestArguments & AndroidLaunchArguments} args
*/
async launchRequest(response, args) {
this.debug_mode = 'launch';
if (args && args.trace) {
this.trace = args.trace;
onMessagePrint(this.LOG.bind(this));
}
D(JSON.stringify({type: 'launch', args, env:process.env}, null, ' '));
if (args.targetDevice === 'null') {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.terminate_reason = "null-targetdevice";
this.sendEvent(new TerminatedEvent(false));
return;
}
D(`Launching: ${JSON.stringify(args)}`);
// app_src_root must end in a path-separator for correct validation of sub-paths
this.app_src_root = ensure_path_end_slash(args.appSrcRoot);
this.apk_fpn = args.apkFile;
this.manifest_fpn = args.manifestFile;
this.pmInstallArgs = args.pmInstallArgs;
this.pm_install_args = args.pmInstallArgs;
this.am_start_args = args.amStartArgs;
if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0)
this.callStackDisplaySize = args.callStackDisplaySize|0;
// set the custom ADB port - this should be changed to pass it to each ADBClient instance
if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) {
ADBSocket.ADBPort = args.adbPort;
// we don't allow both amStartArgs and launchActivity to be specified (the launch activity must be included in amStartArgs)
if (args.amStartArgs && args.launchActivity) {
this.LOG('amStartArgs and launchActivity options cannot both be specified in the launch configuration.');
this.terminate_reason = "amStartArgs+launchActivity";
this.sendEvent(new TerminatedEvent(false));
return;
}
// set the custom ADB host and port
if (typeof args.adbSocket === 'string' && args.adbSocket) {
ADBSocket.HostPort = args.adbSocket;
} else if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) {
ADBSocket.HostPort = `:${args.adbPort}`;
}
// set the fixed JDWP port number (if any)
if (typeof args.jdwpPort === 'number' && args.jdwpPort >= 0 && args.jdwpPort <= 65535) {
Debugger.portManager.fixedport = args.jdwpPort;
}
try {
@@ -283,6 +552,7 @@ class AndroidDebugSession extends DebugSession {
// wow, we really didn't make it very far...
this.LOG(err.message);
this.LOG('Check the "appSrcRoot" and "apkFile" entries in launch.json');
this.terminate_reason = `init-exception: ${err.message}`;
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -299,8 +569,9 @@ class AndroidDebugSession extends DebugSession {
throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json');
// make sure ADB exists and is started and look for a device to install on
await this.checkADBStarted(args.autoStartADB !== false);
this._device = await this.findSuitableDevice(args.targetDevice);
await checkADBStarted(args.autoStartADB !== false);
const targetDevice = this.extractTargetDeviceID(args.targetDevice);
this._device = await this.findSuitableDevice(targetDevice, true);
this._device.adbclient = new ADBClient(this._device.serial);
// install the APK we are going to debug
@@ -314,7 +585,9 @@ class AndroidDebugSession extends DebugSession {
await this.configureAPISourcePath();
// launch the app
await this.startLaunchActivity(args.launchActivity);
await this.startLaunchActivity(args.launchActivity, args.postLaunchPause);
this.debuggerAttached = true;
// if we get this far, the debugger is connected and waiting for the resume command
// - set up some events...
@@ -337,35 +610,41 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response);
await this.dbgr.resume();
analytics.event('debug-started', {
dbg_session_id: this.session_id,
dbg_start: this.session_start.toLocaleTimeString(),
dbg_tz: this.session_start.getTimezoneOffset(),
dbg_kind: 'debug',
dbg_device_api: this.device_api_level,
dbg_emulator: /^emulator/.test(this._device.serial),
dbg_apk_size: this.apk_file_info.file_size,
dbg_pkg_name: this.apk_file_info.manifest.package || '',
})
this.LOG('Application started');
} catch(e) {
const msg = e.message || e.msg;
// exceptions use message, adbclient uses msg
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
this.LOG('Launch failed: '+(msg || 'No additional information is available'));
// more info for adb connect errors
if (/^ADB server is not running/.test(e.msg)) {
this.LOG('Make sure the Android SDK Platform Tools are installed and run:');
this.LOG(' adb start-server');
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
}
if (/ADB|JDWP/.test(msg)) {
this.LOG('Ensure any instances of Android Studio are closed.');
}
// tell the client we're done
this.terminate_reason = `start-exception: ${msg}`;
this.sendEvent(new TerminatedEvent(false));
}
}
async checkADBStarted(autoStartADB) {
const err = await new ADBClient().test_adb_connection();
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
if (err && autoStartADB && process.env.ANDROID_HOME) {
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
const adbargs = ['-P',`${ADBSocket.ADBPort}`,'start-server'];
try {
this.LOG([adbpath, ...adbargs].join(' '));
const stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
this.LOG(stdout);
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
}
}
/**
* Check if the build is out of date (i.e a source file has been modified since the last build)
* @param {StaleBuildSetting} staleBuild
*/
checkBuildIsUpToDate(staleBuild) {
// check if any source file was modified after the apk
if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) {
@@ -378,29 +657,41 @@ class AndroidDebugSession extends DebugSession {
}
}
startLaunchActivity(launchActivity) {
/**
*
* @param {string} launchActivity
* @param {number} postLaunchPause
*/
async startLaunchActivity(launchActivity, postLaunchPause) {
if (!launchActivity) {
// we're allowed no launchActivity if we have a custom am start command
if (!this.am_start_args) {
if (!(launchActivity = this.apk_file_info.manifest.launcher)) {
throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json');
}
}
}
const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity);
this.LOG(`Launching ${build.pkgname}/${launchActivity} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
return this.dbgr.startDebugSession(build, this._device.serial);
const build = new LaunchBuildInfo(
new Map(this.src_packages.packages),
this.apk_file_info.manifest.package,
launchActivity,
this.am_start_args,
postLaunchPause);
this.LOG(`Launching on device ${this._device.serial} [API:${this.device_api_level||'?'}]`);
if (this.am_start_args) {
this.LOG(`Using custom launch arguments '${this.am_start_args.join(' ')}'`);
}
const am_stdout = await this.dbgr.startDebugSession(build, this._device.serial);
this.LOG(am_stdout);
}
async configureAPISourcePath() {
const apilevel = await this.getDeviceAPILevel();
// look for the android sources folder appropriate for this device
if (process.env.ANDROID_HOME && apilevel) {
const sources_path = path.join(process.env.ANDROID_HOME,'sources',`android-${apilevel}`);
fs.stat(sources_path, (err,stat) => {
if (!err && stat && stat.isDirectory())
this._android_sources_path = sources_path;
});
}
this._android_sources_path = getAndroidSourcesFolder(apilevel, true);
}
async getDeviceAPILevel() {
@@ -440,7 +731,8 @@ class AndroidDebugSession extends DebugSession {
})
// send the install command
this.LOG('Installing...');
const command = `pm install ${Array.isArray(this.pmInstallArgs) ? this.pmInstallArgs.join(' ') : '-r'} ${device_apk_fpn}`;
const pm_install_args = Array.isArray(this.pm_install_args) ? this.pm_install_args.join(' ') : '-r';
const command = `pm install ${pm_install_args} ${device_apk_fpn}`;
D(command);
const stdout = await this._device.adbclient.shell_cmd({
command,
@@ -462,11 +754,13 @@ class AndroidDebugSession extends DebugSession {
/**
* @param {string} target_deviceid
* @param {boolean} show_progress
*/
async findSuitableDevice(target_deviceid) {
this.LOG('Searching for devices...');
async findSuitableDevice(target_deviceid, show_progress) {
show_progress && this.LOG('Searching for devices...');
const devices = await this.dbgr.listConnectedDevices()
this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`);
show_progress && this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`);
let reject;
if (devices.length === 0) {
reject = 'No devices are connected';
@@ -495,7 +789,11 @@ class AndroidDebugSession extends DebugSession {
throw new Error(reject);
}
configurationDoneRequest(response/*, args*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.ConfigurationDoneResponse} response
*/
configurationDoneRequest(response) {
D('configurationDoneRequest');
this.waitForConfigurationDone();
this.sendResponse(response);
@@ -511,15 +809,31 @@ class AndroidDebugSession extends DebugSession {
}
}
async disconnectRequest(response/*, args*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.DisconnectResponse} response
*/
async disconnectRequest(response) {
D('disconnectRequest');
this._isDisconnecting = true;
analytics.event('debug-end', {
dbg_session_id: this.session_id,
dbg_elapsed: Math.trunc((Date.now() - this.session_start.getTime())/1e3),
dbg_kind: this.debug_mode,
dbg_term_reason: this.terminate_reason,
});
if (this.debuggerAttached) {
try {
if (this.debug_mode === 'launch') {
await this.dbgr.forceStop();
await this.dbgr.disconnect();
this.LOG(`Debugger stopped`);
} else {
await this.dbgr.disconnect();
this.LOG(`Debugger detached`);
}
} catch (e) {
}
}
this.sendResponse(response);
}
@@ -536,7 +850,7 @@ class AndroidDebugSession extends DebugSession {
}
/**
*
* Called by the debugger in response to a JDWP breakpoint hit event
* @param {JavaBreakpointEvent} e
*/
onBreakpointHit(e) {
@@ -548,8 +862,10 @@ class AndroidDebugSession extends DebugSession {
/**
* Called when the user requests a change to breakpoints in a source file
* Note: all breakpoints in a file are always sent in args, even if they are not changing
* @param {import('vscode-debugprotocol').DebugProtocol.SetBreakpointsResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetBreakpointsArguments} args
*/
async setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) {
async setBreakPointsRequest(response, args) {
const source_filename = args.source && args.source.path;
D('setBreakPointsRequest: ' + source_filename);
@@ -606,10 +922,10 @@ class AndroidDebugSession extends DebugSession {
const bp_queue_len = this._set_breakpoints_queue.push({args,response,relative_fpn});
if (bp_queue_len === 1) {
do {
const next_bp = this._set_breakpoints_queue[0];
const javabp_arr = await this._setup_breakpoints(next_bp);
const { args, relative_fpn, response } = this._set_breakpoints_queue[0];
const javabp_arr = await this.setupBreakpointsInFile(args.breakpoints, relative_fpn);
// send back the VS Breakpoint instances
sendBPResponse(next_bp.response, javabp_arr.map(javabp => javabp.vsbp));
sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
// .. and do the next one
this._set_breakpoints_queue.shift();
} while (this._set_breakpoints_queue.length);
@@ -617,16 +933,13 @@ class AndroidDebugSession extends DebugSession {
}
/**
* @param {*} o
* @param {number} idx
* @param {*[]} javabp_arr
* @param {import('vscode-debugprotocol').DebugProtocol.SourceBreakpoint[]} breakpoints
* @param {string} relative_fpn
*/
async _setup_breakpoints(o, idx = 0, javabp_arr = []) {
const src_bp = o.args.breakpoints[idx];
if (!src_bp) {
// end of list
return javabp_arr;
}
async setupBreakpointsInFile(breakpoints, relative_fpn) {
const java_breakpoints = [];
for (let idx = 0; idx < breakpoints.length; idx++) {
const src_bp = breakpoints[idx];
const dbgline = this.convertClientLineToDebugger(src_bp.line);
const options = new BreakpointOptions();
if (src_bp.hitCondition) {
@@ -640,7 +953,7 @@ class AndroidDebugSession extends DebugSession {
}
}
}
const javabp = await this.dbgr.setBreakpoint(o.relative_fpn, dbgline, options);
const javabp = await this.dbgr.setBreakpoint(relative_fpn, dbgline, options);
if (!javabp.vsbp) {
// state is one of: set,notloaded,enabled,removed
const verified = !!javabp.state.match(/set|enabled/);
@@ -652,11 +965,16 @@ class AndroidDebugSession extends DebugSession {
javabp.vsbp = bp;
}
javabp.vsbp.order = idx;
javabp_arr.push(javabp);
return this._setup_breakpoints(o, ++idx, javabp_arr);
java_breakpoints.push(javabp);
}
return java_breakpoints;
};
async setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.SetExceptionBreakpointsResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetExceptionBreakpointsArguments} args
*/
async setExceptionBreakPointsRequest(response, args) {
await this.dbgr.clearBreakOnExceptions();
switch(true) {
case args.filters.includes('all'):
@@ -669,7 +987,11 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response);
}
async threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
/**
*
* @param {import('vscode-debugprotocol').DebugProtocol.ThreadsResponse} response
*/
async threadsRequest(response) {
if (!this._threads.length) {
try {
await this.refreshThreads();
@@ -693,8 +1015,10 @@ class AndroidDebugSession extends DebugSession {
/**
* Returns a stack trace for the given threadId
* @param {import('vscode-debugprotocol').DebugProtocol.StackTraceResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StackTraceArguments} args
*/
async stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) {
async stackTraceRequest(response, args) {
D(`stackTraceRequest thread:${args.threadId}`);
// only retrieve the stack if the thread is paused
const thread = this.getThread(args.threadId);
@@ -776,7 +1100,11 @@ class AndroidDebugSession extends DebugSession {
}
}
async scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ScopesResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ScopesArguments} args
*/
async scopesRequest(response, args) {
D(`scopesRequest frame:${args.frameId}`);
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
@@ -804,8 +1132,12 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response);
}
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) {
D(`sourceRequest: ${args.sourceId}`);
/**
* @param {import('vscode-debugprotocol').DebugProtocol.SourceResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SourceArguments} args
*/
sourceRequest(response, args) {
D(`sourceRequest: ${args.sourceReference}`);
const content =
`/*
The source for this class is unavailable.
@@ -818,24 +1150,17 @@ class AndroidDebugSession extends DebugSession {
`;
// don't actually attempt to load the file here - just recheck to see if the sources
// path is valid yet.
if (process.env.ANDROID_HOME && this.device_api_level) {
const sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+this.device_api_level);
fs.stat(sources_path, (err,stat) => {
if (!err && stat && stat.isDirectory())
this._android_sources_path = sources_path;
});
}
this._android_sources_path = getAndroidSourcesFolder(this.device_api_level, true);
response.body = { content };
this.sendResponse(response);
}
/**
*
* @param {*} response
* @param {{variablesReference:VSCVariableReference}} args
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.VariablesArguments} args
*/
async variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) {
async variablesRequest(response, args) {
D(`variablesRequest variablesReference:${args.variablesReference}`);
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
const thread = this.getThread(threadId);
@@ -919,7 +1244,11 @@ class AndroidDebugSession extends DebugSession {
await this.dbgr.resumeThread(thread.threadid);
}
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ContinueResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ContinueArguments} args
*/
continueRequest(response, args) {
D(`Continue thread:${args.threadId}`);
const thread = this.getThread(args.threadId);
@@ -942,8 +1271,8 @@ class AndroidDebugSession extends DebugSession {
/**
* Called by the user to start a step operation
* @param {DebuggerStepType} which
* @param {*} response
* @param {*} args
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.NextArguments} args
*/
doStep(which, response, args) {
D(`step ${which}`);
@@ -966,15 +1295,27 @@ class AndroidDebugSession extends DebugSession {
this.dbgr.step(which, thread.threadid);
}
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StepInArguments} args
*/
stepInRequest(response, args) {
this.doStep('in', response, args);
}
nextRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.NextArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.NextArguments} args
*/
nextRequest(response, args) {
this.doStep('over', response, args);
}
stepOutRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepOutArguments*/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.NextResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.StepOutArguments} args
*/
stepOutRequest(response, args) {
this.doStep('out', response, args);
}
@@ -992,7 +1333,11 @@ class AndroidDebugSession extends DebugSession {
this.reportStoppedEvent("exception", e.throwlocation, last_exception);
}
async exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
/**
* @param {import('vscode-debugprotocol').DebugProtocol.ExceptionInfoResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.ExceptionInfoArguments} args
*/
async exceptionInfoRequest(response, args) {
D(`exceptionInfoRequest: ${args.threadId}`);
const thread = this.getThread(args.threadId);
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
@@ -1062,15 +1407,10 @@ class AndroidDebugSession extends DebugSession {
}
/**
* @typedef SetVariableArgs
* @property {string} name
* @property {string} value
* @property {number} variablesReference
*
* @param {*} response
* @param {SetVariableArgs} args
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.SetVariableArguments} args
*/
async setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
async setVariableRequest(response, args) {
const threadId = AndroidThread.variableRefToThreadId(args.variablesReference);
const thread = this.getThread(threadId);
@@ -1082,7 +1422,7 @@ class AndroidDebugSession extends DebugSession {
const stack_frame = thread.findStackFrame(args.variablesReference);
// evaluate the expression
const locals = await stack_frame.getLocals();
const value = await evaluate(args.value, thread, locals, this.dbgr);
const { value } = await evaluate(args.value, thread, locals, this.dbgr);
// update the variable
const vsvar = await stack_frame.setVariableValue(args.variablesReference, args.name, value);
response.body = {
@@ -1099,8 +1439,10 @@ class AndroidDebugSession extends DebugSession {
/**
* Called by VSCode to perform watch, console and hover evaluations
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateArguments} args
*/
async evaluateRequest(response/*: DebugProtocol.EvaluateResponse*/, args/*: DebugProtocol.EvaluateArguments*/) {
async evaluateRequest(response, args) {
// Some notes to remember:
// annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches.
@@ -1120,25 +1462,15 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(prev.response);
}
const eval_info = {
expression: args.expression,
response,
/** @type {DebuggerValue[]} */
locals: null,
/** @type {VariableManager} */
var_manager: null,
/** @type {AndroidThread} */
thread: null,
}
let eval_info;
if (args.frameId) {
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Evaluate',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response);
eval_info.thread = thread;
const stack_frame = thread.findStackFrame(args.frameId);
eval_info.var_manager = stack_frame;
eval_info.locals = await stack_frame.getLocals();
const locals = await stack_frame.getLocals();
eval_info = new EvalQueueEntry(args.expression, response, locals, stack_frame, thread);
} else {
// if there's no frameId, we are being asked to evaluate the value in the 'global' context.
// This is a problem because there's no associated stack frame, so we include any locals in the evaluation.
@@ -1147,9 +1479,7 @@ class AndroidDebugSession extends DebugSession {
// would require primitive literals)
const thread = this._threads.find(t => t && t.paused);
if (!thread) return this.failRequest(`No threads are paused`, response);
eval_info.thread = thread;
eval_info.var_manager = thread.getGlobalVariableManager();
eval_info.locals = [];
eval_info = new EvalQueueEntry(args.expression, response, [], thread.getGlobalVariableManager(), thread);
}
const queue_len = this._evals_queue.push(eval_info);
@@ -1160,8 +1490,8 @@ class AndroidDebugSession extends DebugSession {
while (this._evals_queue.length > 0) {
const { expression, response, locals, var_manager, thread } = this._evals_queue[0];
try {
const value = await evaluate(expression, thread, locals, this.dbgr);
const v = var_manager.makeVariableValue(value);
const { value, display_format } = await evaluate(expression, thread, locals, this.dbgr, { allowFormatSpecifier:true });
const v = var_manager.makeVariableValue(value, display_format);
response.body = {
result: v.value,
variablesReference: v.variablesReference|0
@@ -1176,6 +1506,23 @@ class AndroidDebugSession extends DebugSession {
}
}
class EvalQueueEntry {
/**
* @param {string} expression
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
* @param {DebuggerValue[]} locals
* @param {VariableManager} var_manager
* @param {AndroidThread} thread
*/
constructor(expression, response, locals, var_manager, thread) {
this.expression = expression;
this.response = response;
this.locals = locals;
this.var_manager = var_manager;
this.thread = thread;
}
}
/**
* @param {string} p
*/

View File

@@ -4,18 +4,28 @@ const { PackageInfo } = require('./package-searcher');
const { splitSourcePath } = require('./utils/source-file');
class BuildInfo {
/**
* @param {string} pkgname
* @param {Map<string,PackageInfo>} packages
* @param {string} launchActivity
*/
constructor(pkgname, packages, launchActivity) {
this.pkgname = pkgname;
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 = [
this.startCommandArgs = amCommandArgs || [
'-D', // enable debugging
'--activity-brought-to-front',
'-a android.intent.action.MAIN',
@@ -23,10 +33,19 @@ class BuildInfo {
`-n ${pkgname}/${launchActivity}`,
];
/**
* the amount of time to wait after 'am start ...' is invoked.
* 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 = 1000;
this.postLaunchPause = ((typeof postLaunchPause === 'number') && (postLaunchPause >= 0)) ? postLaunchPause : 1000;
}
}
class AttachBuildInfo extends BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
*/
constructor(packages) {
super(packages);
}
}
@@ -81,10 +100,10 @@ class DebugSession {
this.classPrepareFilters = new Set();
/**
* The set of class signatures already prepared
* The set of class signatures loaded by the runtime
* @type {Set<string>}
*/
this.preparedClasses = new Set();
this.loadedClasses = new Set();
/**
* Enabled step JDWP IDs for each thread
@@ -669,7 +688,7 @@ class DebuggerTypeInfo {
// 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 {JavaType} */
/** @type {JavaClassType} */
this.super = null;
}
}
@@ -746,9 +765,9 @@ class VariableValue {
}
module.exports = {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerException,
DebuggerFrameInfo,
@@ -756,6 +775,7 @@ module.exports = {
DebuggerTypeInfo,
DebugSession,
DebuggerValue,
LaunchBuildInfo,
LiteralValue,
JavaBreakpointEvent,
JavaExceptionEvent,

View File

@@ -8,9 +8,9 @@ const { D } = require('./utils/print');
const { sleep } = require('./utils/thread');
const { decodeJavaStringLiteral } = require('./utils/char-decode');
const {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerFrameInfo,
DebuggerMethodInfo,
@@ -24,6 +24,7 @@ const {
JavaTaggedValue,
JavaThreadInfo,
JavaType,
LaunchBuildInfo,
MethodInvokeArgs,
SourceLocation,
TypeNotAvailable,
@@ -51,9 +52,14 @@ class Debugger extends EventEmitter {
static portManager = {
portrange: { lowest: 31000, highest: 31099 },
fixedport: 0,
inuseports: new Set(),
debuggers: {},
reserveport: function () {
if (this.fixedport > 0 && this.fixedport < 65536) {
this.inuseports.add(this.fixedport);
return this.fixedport;
}
// choose a random port to use each time
for (let i = 0; i < 10000; i++) {
const portidx = this.portrange.lowest + ((Math.random() * 100) | 0);
@@ -71,17 +77,55 @@ class Debugger extends EventEmitter {
};
/**
* @param {BuildInfo} build
* @param {LaunchBuildInfo} build
* @param {string} deviceid
*/
async startDebugSession(build, deviceid) {
if (this.status() !== 'disconnected') {
throw new Error('startDebugSession: session already active');
}
this.session = new DebugSession(build, deviceid);
await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause);
// retrieve the list of debuggable processes
const pids = await this.getDebuggablePIDs(this.session.deviceid);
// choose the last pid in the list
const pid = pids[pids.length - 1];
const named_pids = await Debugger.getDebuggableProcesses(deviceid, 10e3);
if (named_pids.length === 0) {
throw new Error(`startDebugSession: No debuggable processes after app launch.`);
}
// we assume the newly launched app is the last pid in the list, but try and
// validate using the process names
const matched_named_pids = build.pkgname ? named_pids.filter(np => np.name === build.pkgname) : [];
let pid;
switch (matched_named_pids.length) {
case 0:
// no name match - warn, but choose the last entry anyway
D('No process name match - choosing last jdwp pid');
pid = named_pids[named_pids.length - 1].pid;
break;
case 1:
pid = matched_named_pids[0].pid;
break;
default:
// more than one choice - warn, but choose we'll use the last one anyway
D('Multiple process names match - choosing last matching entry');
pid = matched_named_pids[matched_named_pids.length - 1].pid;
break;
}
// after connect(), the caller must call resume() to begin
await this.connect(pid);
return stdout;
}
/**
* @param {AttachBuildInfo} build
* @param {number} pid process ID to connect to
* @param {string} deviceid device ID to connect to
*/
async attachToProcess(build, pid, deviceid) {
if (this.status() !== 'disconnected') {
throw new Error('attachToProcess: session already active')
}
this.session = new DebugSession(build, deviceid);
// after connect(), the caller must call resume() to begin
await this.connect(pid);
}
@@ -89,12 +133,12 @@ class Debugger extends EventEmitter {
/**
* @param {string} deviceid Device ID to connect to
* @param {string[]} launch_cmd_args Array of arguments to pass to 'am start'
* @param {number} [post_launch_pause] amount to time to wait after each launch attempt
* @param {number} post_launch_pause amount of time (in ms) to wait after each launch attempt
*/
static async runApp(deviceid, launch_cmd_args, post_launch_pause = 1000) {
static async runApp(deviceid, launch_cmd_args, post_launch_pause) {
// older (<3) versions of Android only allow target components to be specified with -n
const shell_cmd = {
command: 'am start ' + launch_cmd_args.join(' '),
command: `am start ${launch_cmd_args.join(' ')}`,
};
let retries = 10
for (;;) {
@@ -104,12 +148,14 @@ class Debugger extends EventEmitter {
await sleep(post_launch_pause);
// failures:
// Error: Activity not started...
const m = stdout.match(/Error:.*/g);
// /system/bin/sh: syntax error: unexpected EOF - this happens with invalid am command arguments
const m = stdout.match(/Error:.*|syntax error:/gi);
if (!m) {
break;
// return the stdout from am (it shows the fully qualified component name)
return stdout.toString().trim();
}
else if (retries <= 0){
throw new Error(m[0]);
throw new Error(stdout.toString().trim());
}
retries -= 1;
}
@@ -124,54 +170,21 @@ class Debugger extends EventEmitter {
/**
* Retrieve a list of debuggable process IDs from a device
* @param {string} deviceid
* @param {number} timeout_ms
*/
getDebuggablePIDs(deviceid) {
return new ADBClient(deviceid).jdwp_list();
static getDebuggablePIDs(deviceid, timeout_ms) {
return new ADBClient(deviceid).jdwp_list(timeout_ms);
}
async getDebuggableProcesses(deviceid) {
const adbclient = new ADBClient(deviceid);
const info = {
debugger: this,
jdwps: null,
};
const jdwps = await info.adbclient.jdwp_list();
if (!jdwps.length)
return null;
info.jdwps = jdwps;
// retrieve the ps list from the device
const stdout = await adbclient.shell_cmd({
command: 'ps',
});
// output should look something like...
// USER PID PPID VSIZE RSS WCHAN PC NAME
// u0_a153 32721 1452 1506500 37916 ffffffff 00000000 S com.example.somepkg
// but we cope with variations so long as PID and NAME exist
const lines = stdout.split(/\r?\n|\r/g);
const hdrs = (lines.shift() || '').trim().toUpperCase().split(/\s+/);
const pidindex = hdrs.indexOf('PID');
const nameindex = hdrs.indexOf('NAME');
if (pidindex < 0 || nameindex < 0)
return [];
const result = [];
// scan the list looking for matching pids...
for (let i = 0; i < lines.length; i++) {
const entries = lines[i].trim().replace(/ [S] /, ' ').split(/\s+/);
if (entries.length !== hdrs.length) {
continue;
}
const jdwpidx = info.jdwps.indexOf(entries[pidindex]);
if (jdwpidx < 0) {
continue;
}
// we found a match
const entry = {
jdwp: entries[pidindex],
name: entries[nameindex],
};
result.push(entry);
}
return result;
/**
* Retrieve a list of debuggable process IDs with process names from a device.
* For Android, the process name is usually the package name.
* @param {string} deviceid
* @param {number} timeout_ms
*/
static getDebuggableProcesses(deviceid, timeout_ms) {
return new ADBClient(deviceid).named_jdwp_list(timeout_ms);
}
/**
@@ -224,6 +237,7 @@ class Debugger extends EventEmitter {
async performConnectionTasks() {
// setup port forwarding
// note that this call generally succeeds - even if the JDWP pid is invalid
await new ADBClient(this.session.deviceid).jdwp_forward({
localport: this.connection.localport,
jdwp: this.connection.jdwp,
@@ -233,13 +247,21 @@ class Debugger extends EventEmitter {
// after this, the client keeps an open connection until
// jdwp_disconnect() is called
this.session.adbclient = new ADBClient(this.session.deviceid);
try {
// if the JDWP pid is invalid (doesn't exist, not debuggable, etc) ,this
// is where it will fail...
await this.session.adbclient.jdwp_connect({
localport: this.connection.localport,
onreply: data => this._onJDWPMessage(data),
ondisconnect: () => this._onJDWPDisconnect(),
});
} catch (e) {
// provide a slightly more meaningful message than a socket error
throw new Error(`A debugger connection to pid ${this.connection.jdwp} could not be established. ${e.message}`)
}
// handshake has completed
this.connection.connected = true;
// call suspend first - we shouldn't really need to do this (as the debugger
// is already suspended and will not resume until we tell it), but if we
// don't do this, it logs a complaint...
@@ -255,6 +277,12 @@ class Debugger extends EventEmitter {
// set the class loader event notifier so we can enable breakpoints when the
// runtime loads the classes
await this.initClassPrepareForBreakpoints();
// some types have already been loaded (so we won't receive class-prepare notifications).
// we can't map breakpoint source locations to already-loaded anonymous types, so we just retrieve
// a list of all classes for now.
const all_classes = await this.getAllClasses();
this.session.loadedClasses = new Set(all_classes.map(x => x.signature));
}
/**
@@ -296,8 +324,10 @@ class Debugger extends EventEmitter {
if (!this.session) {
return;
}
if (this.session.build instanceof LaunchBuildInfo) {
return Debugger.forceStopApp(this.session.deviceid, this.session.build.pkgname);
}
}
/**
* Sends a 'am force-stop' command to the given device
@@ -334,17 +364,20 @@ class Debugger extends EventEmitter {
// reset the breakpoint states
this.resetBreakpoints();
this.emit('disconnect');
// clear the session
const adbclient = this.session.adbclient;
this.session = null;
// perform the JDWP disconnect
if (connection.connected) {
await this.session.adbclient.jdwp_disconnect();
await adbclient.jdwp_disconnect();
}
// undo the portforwarding
// todo: replace remove_all with remove_port
if (connection.portforwarding) {
await new ADBClient(this.session.deviceid).forward_remove_all();
await adbclient.forward_remove_all();
}
// mark the port as freed
@@ -352,8 +385,7 @@ class Debugger extends EventEmitter {
Debugger.portManager.freeport(connection.localport);
}
// clear the session
this.session = null;
this.emit('disconnect');
return previous_state;
}
@@ -536,17 +568,12 @@ class Debugger extends EventEmitter {
*/
async initialiseBreakpoint(bp) {
// try and load the class - if the runtime hasn't loaded it yet, this will just return a TypeNotAvailable instance
let classes = [await this.loadClassInfo(`L${bp.qtype};`)];
let classes = await Promise.all(
[...this.session.loadedClasses]
.filter(signature => bp.sigpattern.test(signature))
.map(signature => this.loadClassInfo(signature))
);
let bploc = Debugger.findBreakpointLocation(classes, bp);
if (!bploc) {
// the required location may be inside a nested class (anonymous or named)
// Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here
// is look for existing (cached) loaded types matching inner type signatures
classes = this.session.classList
.filter(c => bp.sigpattern.test(c.type.signature));
// try again
bploc = Debugger.findBreakpointLocation(classes, bp);
}
if (!bploc) {
// we couldn't identify a matching location - either the class is not yet loaded or the
// location doesn't correspond to any code. In case it's the former, make sure we are notified
@@ -680,13 +707,13 @@ class Debugger extends EventEmitter {
}
/**
* @param {DebuggerValue} value
* @param {string} signature
*/
async getSuperType(value) {
if (value.type.signature === JavaType.Object.signature)
async getSuperType(signature) {
if (signature === JavaType.Object.signature)
throw new Error('java.lang.Object has no super type');
const typeinfo = await this.getTypeInfo(value.type.signature);
const typeinfo = await this.getTypeInfo(signature);
await this._ensureSuperType(typeinfo);
return typeinfo.super;
}
@@ -695,7 +722,7 @@ class Debugger extends EventEmitter {
* @param {DebuggerValue} value
*/
async getSuperInstance(value) {
const supertype = await this.getSuperType(value);
const supertype = await this.getSuperType(value.type.signature);
if (value.vtype === 'class') {
return this.getTypeValue(supertype.signature);
}
@@ -741,15 +768,23 @@ class Debugger extends EventEmitter {
}
/**
*
* @param {DebuggerValue} object_value
*/
async getFieldValues(object_value) {
const type = await this.getTypeInfo(object_value.type.signature);
await this._ensureFields(type);
return this.fetchFieldValues(object_value, type.info.typeid, type.fields);
}
/**
* @param {DebuggerValue} object_value
* @param {JavaTypeID} typeid
* @param {JavaField[]} field_list
*/
async fetchFieldValues(object_value, typeid, field_list) {
// the Android runtime now pointlessly barfs into logcat if an instance value is used
// to retrieve a static field. So, we now split into two calls...
const splitfields = type.fields.reduce((z, f) => {
const splitfields = field_list.reduce((z, f) => {
if (f.modbits & 8) {
z.static.push(f);
} else {
@@ -774,7 +809,7 @@ class Debugger extends EventEmitter {
let static_fieldvalues = [];
if (splitfields.static.length) {
static_fieldvalues = await this.session.adbclient.jdwp_command({
cmd: JDWP.Commands.GetStaticFieldValues(type.info.typeid, splitfields.static),
cmd: JDWP.Commands.GetStaticFieldValues(typeid, splitfields.static),
});
}
// make sure the fields and values match up...
@@ -784,7 +819,8 @@ class Debugger extends EventEmitter {
res.forEach((value,i) => {
value.data.field = fields[i];
value.fqname = `${object_value.fqname || object_value.name}.${value.name}`;
})
});
return res;
}
@@ -797,21 +833,24 @@ class Debugger extends EventEmitter {
if (!(object_value.type instanceof JavaClassType)) {
return null;
}
let instance = object_value;
// retrieving field values is expensive, so we search through the class
// fields (which will be cached) until we find a match
let field, object_type = object_value.type, typeinfo;
for (;;) {
// retrieve all the fields for this instance
const fields = await this.getFieldValues(instance);
const field = fields.find(f => f.name === fieldname);
typeinfo = await this.getTypeInfo(object_type.signature);
const fields = await this._ensureFields(typeinfo);
field = fields.find(f => f.name === fieldname);
if (field) {
return field;
break;
}
// if there's no matching field in this instance, check the super
if (!includeInherited || instance.type.signature === JavaType.Object.signature) {
if (!includeInherited || object_type.signature === JavaType.Object.signature) {
const fully_qualified_typename = `${object_value.type.package}.${object_value.type.typename}`;
throw new Error(`No such field '${fieldname}' in type ${fully_qualified_typename}`);
}
instance = await this.getSuperInstance(instance);
object_type = await this.getSuperType(object_type.signature);
}
const values = await this.fetchFieldValues(object_value, typeinfo.info.typeid, [field]);
return values[0];
}
/**
@@ -1489,10 +1528,10 @@ class Debugger extends EventEmitter {
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
// multiple notifications (which duplicates breakpoints, etc)
const signature = prepared_class.type.signature;
if (this.session.preparedClasses.has(signature)) {
if (this.session.loadedClasses.has(signature)) {
return; // we already know about this
}
this.session.preparedClasses.add(signature);
this.session.loadedClasses.add(signature);
D('Prepared: ' + signature);
if (!/^L(.*);$/.test(signature)) {
// unrecognised type signature - ignore it

View File

@@ -471,7 +471,7 @@ async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operat
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} operator
* @param {*} expr
* @param {ParsedExpression} expr
*/
async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) {
/** @type {DebuggerValue} */
@@ -511,8 +511,20 @@ async function evaluate_identifier(dbgr, locals, identifier) {
if (local) {
return local;
}
// check if the identifier is an unqualified member of the current 'this' context
const this_context = locals.find(l => l.name === 'this');
if (this_context) {
try {
const member = await evaluate_member(dbgr, new MemberExpression(identifier), this_context);
return member;
} catch {
// not a member of this - just continue
}
}
// if it's not a local, it could be the start of a package name or a type
const classes = await dbgr.getAllClasses();
const classes = Array.from(dbgr.session.loadedClasses);
return evaluate_qualified_type_name(dbgr, identifier, classes);
}
@@ -520,17 +532,17 @@ async function evaluate_identifier(dbgr, locals, identifier) {
*
* @param {Debugger} dbgr
* @param {string} dotted_name
* @param {*[]} classes
* @param {string[]} classes
*/
async function evaluate_qualified_type_name(dbgr, dotted_name, classes) {
const exact_class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace(/\./g,'[$/]')};$`);
const exact_class = classes.find(c => exact_class_matcher.test(c.type.signature));
const exact_class = classes.find(signature => exact_class_matcher.test(signature));
if (exact_class) {
return dbgr.getTypeValue(exact_class.type.signature);
return dbgr.getTypeValue(exact_class);
}
const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`);
const matching_classes = classes.filter(c => class_matcher.test(c.type.signature));
const matching_classes = classes.filter(signature => class_matcher.test(signature));
if (matching_classes.length === 0) {
// the dotted name doesn't match any packages
throw new Error(`'${dotted_name}' is not a package, type or variable name`);
@@ -623,7 +635,7 @@ async function evaluate_qualifiers(dbgr, locals, thread, value, qualified_terms)
i++;
continue;
}
value = await evaluate_member(dbgr, locals, thread, term, value);
value = await evaluate_member(dbgr, term, value);
continue;
}
if (term instanceof ArrayIndexExpression) {
@@ -684,7 +696,7 @@ function evaluate_expression(dbgr, locals, thread, expr) {
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} index_expr
* @param {ParsedExpression} index_expr
* @param {DebuggerValue} arr_local
*/
async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) {
@@ -712,36 +724,36 @@ async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_loca
/**
* Build a regular expression which matches the possible parameter types for a value
* @param {Debugger} dbgr
* @param {DebuggerValue} v
* @param {DebuggerValue} argument
*/
async function getParameterSignatureRegex(dbgr, v) {
if (v.type.signature == 'Lnull;') {
async function getParameterSignatureRegex(dbgr, argument) {
if (argument.type.signature == 'Lnull;') {
return /^[LT[]/; // null matches any reference type
}
if (/^L/.test(v.type.signature)) {
if (/^L/.test(argument.type.signature)) {
// for class reference types, retrieve a list of inherited classes
// since subclass instances can be passed as arguments
const sigs = await dbgr.getClassInheritanceList(v.type.signature);
const sigs = await dbgr.getClassInheritanceList(argument.type.signature);
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
}
if (/^\[/.test(v.type.signature)) {
if (/^\[/.test(argument.type.signature)) {
// for array types, only an exact array match or Object is allowed
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
return new RegExp(`^(${argument.type.signature})|(${JavaType.Object.signature})$`);
}
switch(v.type.signature) {
switch(argument.type.signature) {
case 'I':
// match bytes/shorts/ints/longs/floats/doubles literals within range
if (v.value >= -128 && v.value <= 127)
if (argument.value >= -128 && argument.value <= 127)
return /^[BSIJFD]$/
if (v.value >= -32768 && v.value <= 32767)
if (argument.value >= -32768 && argument.value <= 32767)
return /^[SIJFD]$/
return /^[IJFD]$/;
case 'F':
return /^[FD]$/; // floats can be assigned to floats or doubles
default:
// anything else must be an exact match (no implicit cast is valid)
return new RegExp(`^${v.type.signature}$`);
return new RegExp(`^${argument.type.signature}$`);
}
}
@@ -822,12 +834,10 @@ async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_loc
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {MemberExpression} member
* @param {DebuggerValue} value
*/
async function evaluate_member(dbgr, locals, thread, member, value) {
async function evaluate_member(dbgr, member, value) {
if (!JavaType.isReference(value.type)) {
throw new Error('TypeError: value is not a reference type');
}
@@ -950,8 +960,9 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
* @param {{allowFormatSpecifier:boolean}} [options]
*/
async function evaluate(expression, thread, locals, dbgr) {
async function evaluate_one_expression(expression, thread, locals, dbgr, options) {
D('evaluate: ' + expression);
await dbgr.ensureConnected();
@@ -967,6 +978,17 @@ async function evaluate(expression, thread, locals, dbgr) {
}
const parsed_expression = parse_expression(e);
let display_format = null;
if (options && options.allowFormatSpecifier) {
// look for formatting specifiers in the form of ',<x>'
// ref: https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp
const df_match = e.expr.match(/^,([doc!]|[xX]b?|bb?|sb?)/);
if (df_match) {
display_format = df_match[1];
e.expr = e.expr.slice(df_match[0].length)
}
}
// if there's anything left, it's an error
if (!parsed_expression || e.expr) {
// the expression is not well-formed
@@ -975,8 +997,51 @@ async function evaluate(expression, thread, locals, dbgr) {
// the expression is well-formed - start the (asynchronous) evaluation
const value = await evaluate_expression(dbgr, locals, thread, parsed_expression);
return value;
return {
value,
display_format,
}
}
/**
*
*/
const queuedExpressions = [];
/**
* @param {string} expression
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
* @param {{allowFormatSpecifier:boolean}} [options]
*/
async function evaluate(expression, thread, locals, dbgr, options) {
return new Promise(async (resolve, reject) => {
const queue_length = queuedExpressions.push({
expression, thread, locals, dbgr, options,
resolve, reject
});
if (queue_length > 1) {
return;
}
// run the queue
while (queuedExpressions.length) {
const {
expression, thread, locals, dbgr, options,
resolve, reject
} = queuedExpressions[0];
try {
const res = await evaluate_one_expression(expression, thread, locals, dbgr, options);
resolve(res);
} catch (err) {
reject(err);
}
queuedExpressions.shift();
}
});
}
module.exports = {
evaluate,

View File

@@ -51,7 +51,7 @@ class RootExpression extends ParsedExpression {
class TypeCastExpression extends ParsedExpression {
/**
*
* @param {string} cast_type
* @param {ParsedExpression} cast_type
* @param {ParsedExpression} rhs
*/
constructor(cast_type, rhs) {
@@ -87,11 +87,29 @@ class UnaryOpExpression extends ParsedExpression {
}
}
class IncOpExpression extends ParsedExpression {
/**
* @param {'e++'|'e--'|'++e'|'--e'} which
* @param {ParsedExpression} expression
*/
constructor(which, expression) {
super();
this.which = which;
this.expression = expression;
}
}
class TernaryExpression extends ParsedExpression {
/**
* @param {ParsedExpression} condition
*/
constructor(condition) {
super();
this.condition = condition;
/** @type {ParsedExpression} */
this.ternary_true = null;
/** @type {ParsedExpression} */
this.ternary_false = null;
}
}
@@ -101,23 +119,72 @@ class QualifierExpression extends ParsedExpression {
}
class ArrayIndexExpression extends QualifierExpression {
constructor(e) {
/**
* @param {ParsedExpression} index_expression
*/
constructor(index_expression) {
super();
this.indexExpression = e;
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
@@ -201,17 +268,21 @@ function parseBracketOrCastExpression(e) {
if (!strip(e, ')')) {
return null;
}
if (res instanceof RootExpression) {
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) {
// primitive typecast
const castexpr = parse_expression_term(e);
// 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;
}
res = new TypeCastExpression(res.root_term, castexpr);
return new TypeCastExpression(res, castexpr);
}
}
return res;
const qt = parse_qualified_terms(e);
return new BracketedExpression(res, qt);
}
/**
@@ -232,19 +303,124 @@ function parseUnaryExpression(e, unop) {
return res;
}
/**
* @param {ExpressionText} e
* @param {RootExpression} ctr
* @param {QualifierExpression[]} ctr_qualifiers
* @param {QualifierExpression[]} post_ctr_qualifiers
*/
function parseNewObjectExpression(e, ctr, ctr_qualifiers, post_ctr_qualifiers) {
const ctr_call = new RootExpression(ctr.root_term, ctr.root_term_type, ctr_qualifiers);
let method_body = null;
if (!post_ctr_qualifiers.length) {
// if there are no qualifiers following the constructor, look for an anonymous method body
if (e.expr.startsWith('{')) {
// don't parse it - just scan for the closing brace
const brace_re = /\/\*[\d\D]*?\*\/|\/\/.*|".*?"|".*|'.'?|(\{)|(\})/g;
let balance = 0, body_end = e.expr.length;
for (let m; m = brace_re.exec(e.expr); ) {
if (m[1]) balance++;
else if (m[2] && (--balance === 0)) {
body_end = m.index + 1;
break;
}
}
method_body = e.expr.slice(0, body_end);
strip(e, method_body.length);
}
}
return new NewObjectExpression(ctr_call, post_ctr_qualifiers, method_body);
}
/**
* @param {ExpressionText} e
* @param {RootExpression} ctr
* @param {Number} first_array_qualifier_idx
*/
function parseNewArrayExpression(e, ctr, first_array_qualifier_idx) {
let arrdim_initers = [];
let i = first_array_qualifier_idx;
for (; i < ctr.qualified_terms.length; i++) {
const term = ctr.qualified_terms[i];
if (term instanceof ArrayIndexExpression) {
arrdim_initers.push(term);
continue;
}
break;
}
const type = new RootExpression(ctr.root_term, ctr.root_term_type, ctr.qualified_terms.slice(0, first_array_qualifier_idx));
return new NewArrayExpression(type, arrdim_initers, ctr.qualified_terms.slice(i));
}
/**
* @param {ExpressionText} e
*/
function parseNewExpression(e) {
const ctr = parse_expression_term(e);
if (!(ctr instanceof RootExpression)) {
return null;
}
let new_expression = null;
ctr.qualified_terms.find((qt,idx) => {
if (qt instanceof MethodCallExpression) {
// new object contructor - split into constructor qualifiers and post-ctr-qualifiers
const qualified_terms = ctr.qualified_terms.slice();
const ctr_qualifiers = qualified_terms.splice(0, idx + 1);
new_expression = parseNewObjectExpression(e, ctr, ctr_qualifiers, qualified_terms);
return true;
}
if (qt instanceof ArrayIndexExpression) {
// new array constructor
// in java, multi-dimensional array constructors have priority over array accessors
// e.g new int[2][1] - is a 2D array,
// (new int[2])[1] - is the 1th element of a 1D array
new_expression = parseNewArrayExpression(e, ctr, idx);
return true;
}
});
if (!new_expression) {
// treat unqualified new expressions as object constructors with no parameters
// eg. new XYZ === new XYZ()
new_expression = parseNewObjectExpression(e, ctr, ctr.qualified_terms, []);
}
return new_expression;
}
/**
* @param {ExpressionText} e
*/
function parseArrayLiteral(e) {
const arr = new ArrayLiteralExpression();
if (!strip(e, '}')) {
for (let element; ;) {
if ((element = parse_expression(e)) === null) {
return null;
}
arr.elements.push(element);
if (strip(e, ',')) continue;
if (strip(e, '}')) break;
return null;
}
}
return arr;
}
/**
* @param {ExpressionText} e
*/
function parse_expression_term(e) {
if (e.expr[0] === '(') {
return parseBracketOrCastExpression(new ExpressionText(e.expr));
const subexpr = new ExpressionText(e.expr);
const bexpr = parseBracketOrCastExpression(subexpr);
e.expr = subexpr.expr;
return bexpr;
}
const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
return parseUnaryExpression(e, unop[0]);
}
const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'];
const root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
let root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) {
return null;
}
@@ -254,10 +430,6 @@ function parse_expression_term(e) {
if (qualified_terms === null) {
return null;
}
// the root term is not allowed to be a method call
if (qualified_terms[0] instanceof MethodCallExpression) {
return null;
}
return new RootExpression(root_term[0], root_term_type, qualified_terms);
}
@@ -265,16 +437,45 @@ function parse_expression_term(e) {
* @param {string} s
*/
function getBinaryOperator(s) {
const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
const binary_op_match = s.match(/^([!=/%*^+-]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
return binary_op_match ? binary_op_match[0] : null;
}
/**
* @param {ExpressionText} e
* @param {ExpressionText|string} e
* @returns {ParsedExpression}
*/
function parse_expression(e) {
if (typeof e === 'string') {
e = new ExpressionText(e);
}
const newop = e.expr.match(/^new\b/);
if (newop) {
strip(e, 3);
return parseNewExpression(e);
}
const arrayinit = e.expr.match(/^\{/);
if (arrayinit) {
strip(e, 1);
return parseArrayLiteral(e);
}
const prefix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?=[a-zA-Z_])/);
if (prefix_incdec) {
strip(e, 2);
}
let res = parse_expression_term(e);
if (prefix_incdec) {
res = new IncOpExpression(e.expr[1] ? '++e' : '--e', res);
}
const postfix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?![+-])/);
if (postfix_incdec) {
if (prefix_incdec) {
return null;
}
strip(e, 2);
res = new IncOpExpression(e.expr[1] ? 'e++' : 'e--', res);
}
for (; ;) {
const binary_operator = getBinaryOperator(e.expr);
@@ -314,6 +515,8 @@ module.exports = {
ExpressionText,
MemberExpression,
MethodCallExpression,
NewArrayExpression,
NewObjectExpression,
parse_expression,
ParsedExpression,
QualifierExpression,

View File

@@ -108,6 +108,7 @@
* @typedef {number} JDWPRequestID
* @typedef {JDWPRequestID} StepID
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
* @typedef {'ignore'|'warn'|'stop'} StaleBuildSetting
*
*/

View File

@@ -1534,7 +1534,7 @@ class JDWP {
const res = [];
let arrlen = DataCoder.decodeInt(o);
while (--arrlen >= 0) {
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{type:'signature'},{genericSignature:'string'},{status:'status'}]));
res.push(DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'},{signature:'string'},{genericSignature:'string'},{status:'status'}]));
}
return res;
}

View File

@@ -7,6 +7,8 @@ const WebSocketServer = require('ws').Server;
// our stuff
const { ADBClient } = require('./adbclient');
const { AndroidContentProvider } = require('./contentprovider');
const { checkADBStarted } = require('./utils/android');
const { selectTargetDevice } = require('./utils/device');
const { D } = require('./utils/print');
/**
@@ -292,83 +294,58 @@ function onWebSocketClientConnection(client, req) {
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
}
function getADBPort() {
const defaultPort = 5037;
const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
}
function openLogcatWindow(vscode) {
new ADBClient().test_adb_connection()
.then(err => {
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
const adbport = getADBPort();
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
const adbargs = ['-P',''+adbport,'start-server'];
try {
/*const stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
}
})
.then(() => new ADBClient().list_devices())
.then(devices => {
switch(devices.length) {
case 0:
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
return null;
case 1:
return devices; // only one device - just show it
}
const prefix = 'Android: View Logcat - ', all = '[ Display All ]';
const devicelist = devices.map(d => prefix + d.serial);
//devicelist.push(prefix + all);
return vscode.window.showQuickPick(devicelist)
.then(which => {
if (!which) return; // user cancelled
which = which.slice(prefix.length);
return new ADBClient().list_devices()
.then(devices => {
if (which === all) {
return devices
}
const found = devices.find(d => d.serial === which);
if (found) {
return [found];
}
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
return null;
});
}, () => null);
})
.then(devices => {
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
devices.forEach(device => {
if (vscode.window.createWebviewPanel) {
/**
* @param {import('vscode')} vscode
* @param {*} target_device
*/
function openWebviewLogcatWindow(vscode, target_device) {
const panel = vscode.window.createWebviewPanel(
'androidlogcat', // Identifies the type of the webview. Used internally
`logcat-${device.serial}`, // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
`logcat-${target_device.serial}`, // Title of the panel displayed to the user
vscode.ViewColumn.Two, // Editor column to show the new webview panel in.
{
enableScripts: true,
enableScripts: true, // we use embedded scripts to relay logcat info over a websocket
}
);
const logcat = new LogcatContent(device.serial);
const logcat = new LogcatContent(target_device.serial);
logcat.content().then(html => {
panel.webview.html = html;
});
}
/**
* @param {import('vscode')} vscode
* @param {*} target_device
*/
function openPreviewHtmlLogcatWindow(vscode, target_device) {
const uri = AndroidContentProvider.getReadLogcatUri(target_device.serial);
vscode.commands.executeCommand("vscode.previewHtml", uri, vscode.ViewColumn.Two);
}
/**
* @param {import('vscode')} vscode
*/
async function openLogcatWindow(vscode) {
try {
// if adb is not running, see if we can start it ourselves
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
await checkADBStarted(autoStartADB);
let target_device = await selectTargetDevice(vscode, "Logcat display");
if (!target_device) {
return;
}
const uri = AndroidContentProvider.getReadLogcatUri(device.serial);
vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
});
})
.catch((/*e*/) => {
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
});
if (vscode.window.createWebviewPanel) {
// newer versions of vscode use WebviewPanels
openWebviewLogcatWindow(vscode, target_device);
} else {
// older versions of vscode use previewHtml
openPreviewHtmlLogcatWindow(vscode, target_device);
}
} catch (e) {
vscode.window.showInformationMessage(`Logcat cannot be displayed. ${e.message}`);
}
}
module.exports = {

77
src/process-attach.js Normal file
View File

@@ -0,0 +1,77 @@
const os = require('os');
const { ADBClient } = require('./adbclient');
/**
* @param {import('vscode')} vscode
* @param {{pid:number,name:string}[]} pids
*/
async function showPIDPicker(vscode, pids) {
// sort by PID (the user can type the package name to search)
const sorted_pids = pids.slice().sort((a,b) => a.pid - b.pid);
/** @type {import('vscode').QuickPickItem[]} */
const device_pick_items = sorted_pids
.map(x => ({
label: `${x.pid}`,
description: x.name,
}));
/** @type {import('vscode').QuickPickOptions} */
const device_pick_options = {
matchOnDescription: true,
canPickMany: false,
placeHolder: 'Choose the Android process to attach to',
};
const chosen_option = await vscode.window.showQuickPick(device_pick_items, device_pick_options);
return sorted_pids[device_pick_items.indexOf(chosen_option)] || null;
}
/**
* @param {import('vscode')} vscode
* @param {string} device_serial
*/
async function selectAndroidProcessID(vscode, device_serial) {
const res = {
/** @type {string|'ok'|'cancelled'|'failed'} */
status: 'failed',
pid: 0,
serial: '',
}
let named_pids;
try {
named_pids = await new ADBClient(device_serial).named_jdwp_list(5000);
} catch {
vscode.window.showWarningMessage(`Attach failed. Check the device ${device_serial} is connected.`);
return res;
}
if (named_pids.length === 0) {
vscode.window.showWarningMessage(
'Attach failed. No debuggable processes are running on the device.'
+ `${os.EOL}${os.EOL}`
+ `To allow a debugger to attach, the app must have the "android:debuggable=true" attribute present in AndroidManifest.xml and be running on the device.`
+ `${os.EOL}`
+ `See https://developer.android.com/guide/topics/manifest/application-element#debug`
);
return res;
}
// always show the pid picker - even if there's only one
const named_pid = await showPIDPicker(vscode, named_pids);
if (named_pid === null) {
// user cancelled picker
res.status = 'cancelled';
return res;
}
res.pid = named_pid.pid;
res.serial = device_serial;
res.status = 'ok';
return res;
}
module.exports = {
selectAndroidProcessID,
}

View File

@@ -7,10 +7,17 @@ const AndroidSocket = require('./androidsocket');
class ADBSocket extends AndroidSocket {
/**
* The port number to run ADB on.
* The value can be overriden by the adbPort value in each configuration.
* The host and port number to run ADB commands on, in 'host:port' format (host part is optional).
* The value can be overriden by the adbSocket (or the deprecated adbPort) value in each debug configuration.
*
* The default host value is left blank as this is the simplest way to
* specify "connect to the local machine" without explicitly specifying
* 'localhost' or '127.0.0.1' (which may be mapped to something else)
*/
static ADBPort = 5037;
static HostPort = `:5037`;
static get DefaultHostPort() {
return `:5037`
}
constructor() {
super('ADBSocket');
@@ -18,13 +25,14 @@ class ADBSocket extends AndroidSocket {
/**
* Reads and checks the reply from an ADB command
* @param {string} command
* @param {boolean} [throw_on_fail] true if the function should throw on non-OKAY status
*/
async read_adb_status(throw_on_fail = true) {
async read_adb_status(command, throw_on_fail = true) {
// read back the status
const status = await this.read_bytes(4, 'latin1')
if (status !== 'OKAY' && throw_on_fail) {
throw new Error(`ADB command failed. Status: '${status}'`);
throw new Error(`ADB command '${command}' failed. Status: '${status}'`);
}
return status;
}
@@ -57,7 +65,7 @@ class ADBSocket extends AndroidSocket {
*/
async cmd_and_status(command) {
await this.write_adb_command(command);
return this.read_adb_status();
return this.read_adb_status(command);
}
/**
@@ -73,10 +81,13 @@ class ADBSocket extends AndroidSocket {
/**
* Sends an ADB command, checks the returned status and then reads raw data from the socket
* @param {string} command
* @param {number} timeout_ms
* @param {boolean} [until_closed]
*/
async cmd_and_read_stdout(command) {
async cmd_and_read_stdout(command, timeout_ms, until_closed) {
await this.cmd_and_status(command);
return this.read_stdout();
const buf = await this.read_stdout(timeout_ms, until_closed);
return buf.toString('latin1');
}
/**
@@ -101,7 +112,7 @@ class ADBSocket extends AndroidSocket {
await this.write_bytes(done_and_mtime);
// read the final status and any error message
const result = await this.read_adb_status(false);
const result = await this.read_adb_status('sync:', false);
const failmsg = await this.read_le_length_data('latin1');
// finish the transfer mode

View File

@@ -64,8 +64,9 @@ class AndroidSocket extends EventEmitter {
*
* @param {number|'length+data'|undefined} length
* @param {string} [format]
* @param {number} [timeout_ms]
*/
async read_bytes(length, format) {
async read_bytes(length, format, timeout_ms) {
//D(`reading ${length} bytes`);
let actual_length = length;
if (typeof actual_length === 'undefined') {
@@ -95,25 +96,40 @@ class AndroidSocket extends EventEmitter {
return Promise.resolve(data);
}
// wait for the socket to update and then retry the read
await this.wait_for_socket_data();
await this.wait_for_socket_data(timeout_ms);
return this.read_bytes(length, format);
}
wait_for_socket_data() {
/**
*
* @param {number} [timeout_ms]
*/
wait_for_socket_data(timeout_ms) {
return new Promise((resolve, reject) => {
let done = 0;
let done = 0, timer = null;
let onDataChanged = () => {
if ((done += 1) !== 1) return;
this.off('socket-ended', onSocketEnded);
clearTimeout(timer);
resolve();
}
let onSocketEnded = () => {
if ((done += 1) !== 1) return;
this.off('data-changed', onDataChanged);
clearTimeout(timer);
reject(new Error(`${this.which} socket read failed. Socket closed.`));
}
let onTimerExpired = () => {
if ((done += 1) !== 1) return;
this.off('socket-ended', onSocketEnded);
this.off('data-changed', onDataChanged);
reject(new Error(`${this.which} socket read failed. Read timeout.`));
}
this.once('data-changed', onDataChanged);
this.once('socket-ended', onSocketEnded);
if (typeof timeout_ms === 'number' && timeout_ms >= 0) {
timer = setTimeout(onTimerExpired, timeout_ms);
}
});
}
@@ -122,8 +138,26 @@ class AndroidSocket extends EventEmitter {
return this.read_bytes(len.readUInt32LE(0), format);
}
read_stdout(format = 'latin1') {
return this.read_bytes(undefined, format);
/**
*
* @param {number} [timeout_ms]
* @param {boolean} [until_closed]
* @returns {Promise<Buffer>}
*/
async read_stdout(timeout_ms, until_closed) {
let buf = await this.read_bytes(undefined, null, timeout_ms);
if (!until_closed) {
return buf;
}
const parts = [buf];
try {
for (;;) {
buf = await this.read_bytes(undefined, null);
parts.push(buf);
}
} catch {
}
return Buffer.concat(parts);
}
/**
@@ -134,6 +168,7 @@ class AndroidSocket extends EventEmitter {
return new Promise((resolve, reject) => {
this.check_socket_active('write');
try {
// @ts-ignore
const flushed = this.socket.write(bytes, () => {
flushed ? resolve() : this.socket.once('drain', resolve);
});

Some files were not shown because too many files have changed in this diff Show More