first hacky version of source parsing and type checking

This commit is contained in:
Dave Holoway
2020-05-16 16:59:58 +01:00
parent 3156a2ddc2
commit 79bba76b9f
22 changed files with 2646 additions and 289 deletions

View File

@@ -11,327 +11,498 @@ const {
CompletionItem,
CompletionItemKind,
TextDocumentSyncKind,
Position,
//TextDocumentPositionParams
} = require('vscode-languageserver');
const { TextDocument } = require('vscode-languageserver-textdocument');
} = require('vscode-languageserver');
const MTI = require('./java/mti');
let androidLibrary = null;
function loadAndroidLibrary(retry) {
const { TextDocument } = require('vscode-languageserver-textdocument');
const MTI = require('./java/mti');
const { parse, ParseProblem, ProblemSeverity, ParseResult } = require('./java/parser');
const { resolveImports } = require('./java/import-resolver');
let androidLibrary = null;
function loadAndroidLibrary(retry) {
try {
androidLibrary = MTI.unpackJSON('/tmp/jarscanner/android-25/android-25.json');
connection.console.log(`Android type cache loaded: ${androidLibrary.types.length} types from ${androidLibrary.packages.length} packages.`);
androidLibrary = MTI.unpackJSON('/tmp/jarscanner/android-25/android-25.json');
connection.console.log(`Android type cache loaded: ${androidLibrary.types.length} types from ${androidLibrary.packages.length} packages.`);
} catch (e) {
connection.console.log(`Failed to load android type cache`);
if (retry) {
return;
}
connection.console.log(`Rebuilding type cache...`);
const jarscanner = require(`jarscanner/jarscanner`);
fs.mkdir('/tmp/jarscanner', err => {
if (err) {
connection.console.log(`Cannot create type cache folder. ${err.message}.`);
return
connection.console.log(`Failed to load android type cache`);
if (retry) {
return;
}
jarscanner.process_android_sdk_source({
destpath: '/tmp/jarscanner',
sdkpath: process.env['ANDROID_SDK'],
api: 25,
cleandest: true,
}, (err) => {
if (err) {
connection.console.log(`Android cache build failed. ${err.message}.`);
return
}
loadAndroidLibrary(true);
})
})
connection.console.log(`Rebuilding type cache...`);
const jarscanner = require(`jarscanner/jarscanner`);
fs.mkdir('/tmp/jarscanner', (err) => {
if (err && err.errno !== -17) {
connection.console.log(`Cannot create type cache folder. ${err.message}.`);
return;
}
jarscanner.process_android_sdk_source(
{
destpath: '/tmp/jarscanner',
sdkpath: process.env['ANDROID_SDK'],
api: 25,
cleandest: true,
},
(err) => {
if (err) {
connection.console.log(`Android cache build failed. ${err.message}.`);
return;
}
loadAndroidLibrary(true);
}
);
});
}
}
}
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);
// Create a simple text document manager. The text document manager
// supports full document sync only
let documents = new TextDocuments({
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);
/**
* @typedef LiveParseInfo
* @property {string} uri
* @property {JavaTokenizer.LineInfo[]} lines
* @property {{startState: string, states: string[], endState: string}[]} states
*/
///** @type {LiveParseInfo[]} */
//const liveParsers = [];
/** @type {{content: string, uri: string, result: ParseResult, positionAt:(n) => Position, indexAt:(p:Position) => number}} */
let parsed = null;
function reparse(uri, content) {
const result = parse(content);
parsed = {
content,
uri,
result,
positionAt(n) {
let line = 0,
last_nl_idx = 0,
character = 0;
if (n <= 0) return { line, character };
for (let idx = 0; ;) {
idx = this.content.indexOf('\n', idx) + 1;
if (idx === 0 || idx > n) {
if (idx === 0) n = content.length;
character = n - last_nl_idx;
return { line, character };
}
last_nl_idx = idx;
line++;
}
},
indexAt(pos) {
let idx = 0;
for (let i = 0; i < pos.line; i++) {
idx = this.content.indexOf('\n', idx) + 1;
if (idx === 0) {
return this.content.length;
}
}
return Math.min(idx + pos.character, this.content.length);
},
};
}
// Create a simple text document manager. The text document manager
// supports full document sync only
let documents = new TextDocuments({
/**
*
* @param {string} uri
* @param {string} languageId
* @param {number} version
* @param {string} content
*
* @param {string} uri
* @param {string} languageId
* @param {number} version
* @param {string} content
*/
create(uri, languageId, version, content) {
connection.console.log(JSON.stringify({what:'create',uri,languageId,version,content}));
//connection.console.log(JSON.stringify({what:'create',uri,languageId,version,content}));
// tokenize the file content and build the initial parse state
connection.console.log(`create parse ${version}`);
reparse(uri, content);
//connection.console.log(res.imports.length.toString());
// const lines = JavaTokenizer.get().tokenizeSource(content);
// const initialParse = new JavaParser().parseLines(lines);
// liveParsers.push({
// uri,
// lines,
// states: initialParse,
// })
// console.log(initialParse.map(x => x.decls).filter(x => x.length).map(x => JSON.stringify(x, null, ' ')));
return { uri };
},
/**
*
* @param {*} document
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
* @param {number} version
*
* @param {TextDocument} document
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
* @param {number} version
*/
update(document, changes, version) {
connection.console.log(JSON.stringify({what:'update',changes,version}));
}
connection.console.log(JSON.stringify({ what: 'update', changes, version }));
//connection.console.log(`update ${version}`);
//return document;
if (parsed && document && parsed.uri === document.uri) {
changes.forEach((change) => {
/** @type {import('vscode-languageserver').Range} */
const r = change['range'];
if (r) {
const start_index = parsed.indexAt(r.start);
let end_index = start_index + (r.end.character - r.start.character);
if (r.end.line !== r.start.line) end_index = parsed.indexAt(r.end);
parsed.content = `${parsed.content.slice(0, start_index)}${change.text}${parsed.content.slice(end_index)}`;
}
});
//connection.console.log(JSON.stringify(parsed.content));
reparse(document.uri, parsed.content);
}
return document;
},
});
});
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
connection.onInitialize((params) => {
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
connection.onInitialize((params) => {
process.nextTick(loadAndroidLibrary);
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;
hasConfigurationCapability = capabilities.workspace && !!capabilities.workspace.configuration;
hasWorkspaceFolderCapability =
capabilities.workspace && !!capabilities.workspace.workspaceFolders;
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
hasDiagnosticRelatedInformationCapability =
capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation;
capabilities.textDocument && capabilities.textDocument.publishDiagnostics && capabilities.textDocument.publishDiagnostics.relatedInformation;
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: true
}
}
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: true,
},
},
};
});
connection.onInitialized(() => {
});
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(DidChangeConfigurationNotification.type, undefined);
// Register for all configuration changes.
connection.client.register(DidChangeConfigurationNotification.type, undefined);
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log('Workspace folder change event received.');
});
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
connection.console.log('Workspace folder change event received.');
});
}
});
// The example settings
/**
* @typedef ExampleSettings
* @property {number} maxNumberOfProblems
*/
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings = { maxNumberOfProblems: 1000 };
let globalSettings = defaultSettings;
// Cache the settings of all open documents
/** @type {Map<string, Thenable<ExampleSettings>>} */
let documentSettings = new Map();
connection.onDidChangeConfiguration(change => {
});
// The example settings
/**
* @typedef ExampleSettings
* @property {number} maxNumberOfProblems
*/
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings = { maxNumberOfProblems: 1000 };
let globalSettings = defaultSettings;
// Cache the settings of all open documents
/** @type {Map<string, Thenable<ExampleSettings>>} */
let documentSettings = new Map();
connection.onDidChangeConfiguration((change) => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear();
// Reset all cached document settings
documentSettings.clear();
} else {
globalSettings = (
(change.settings.androidJavaLanguageServer || defaultSettings)
);
globalSettings = change.settings.androidJavaLanguageServer || defaultSettings;
}
// Revalidate all open text documents
documents.all().forEach(validateTextDocument);
});
function getDocumentSettings(resource) {
});
function getDocumentSettings(resource) {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings);
return Promise.resolve(globalSettings);
}
let result = documentSettings.get(resource);
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'androidJavaLanguageServer'
});
documentSettings.set(resource, result);
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'androidJavaLanguageServer',
});
documentSettings.set(resource, result);
}
return result;
}
// Only keep settings for open documents
documents.onDidClose(e => {
}
// Only keep settings for open documents
documents.onDidClose((e) => {
connection.console.log('doc closed');
parsed = null;
documentSettings.delete(e.document.uri);
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
// documents.onDidChangeContent(change => {
// connection.console.log(JSON.stringify(change));
//validateTextDocument(change.document);
// });
/**
* @param {TextDocument} textDocument
*/
async function validateTextDocument(textDocument) {
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent((change) => {
connection.console.log(JSON.stringify(change));
validateTextDocument(change.document);
});
/**
* @param {{uri}} textDocument
*/
async function validateTextDocument(textDocument) {
/** @type {ParseProblem[]} */
let problems = [];
connection.console.log('validateTextDocument');
if (parsed && parsed.result) {
// package problem
if (parsed.result.package) {
problems = [...problems, ...parsed.result.package.validate()];
}
// import problems
problems = parsed.result.imports.reduce((problems, import_decl) => {
return [...problems, ...import_decl.validate()];
}, problems);
// type problems
problems = parsed.result.types.reduce((problems, type_decl) => {
return [...problems, ...type_decl.validate()];
}, problems);
// syntax problems
problems = parsed.result.invalids.reduce((problems, invalid) => {
return [...problems, ...invalid.validate()];
}, problems);
const package_name = parsed.result.package ? parsed.result.package.dottedName() : '';
const source_mtis = parsed.result.types.map(type_decl => {
return new MTI().addType(package_name, type_decl.getDocString(), type_decl.getAccessModifierValues(), type_decl.kind, type_decl.qualifiedName());
})
const imports = resolveImports(androidLibrary, parsed.result.imports, package_name, source_mtis);
// missing/invalid imports
problems = imports.unresolved.reduce((problems, unresolved) => {
const fqn = unresolved.nameparts.join('.');
return [...problems, new ParseProblem(unresolved.nameparts, `Unresolved import: ${fqn}`, ProblemSeverity.Warning)];
}, problems);
// resolved types
problems = parsed.result.types.reduce((problems, type_decl) => {
return [...problems, ...type_decl.validateTypes(package_name, imports.resolved, imports.typemap)];
}, problems);
// duplicate type names
/** @type {Map<string,import('./java/parsetypes/type')[]>} */
const typenames = new Map();
parsed.result.types.forEach(type_decl => {
const qname = type_decl.qualifiedName();
let list = typenames.get(qname);
if (!list) typenames.set(qname, list = []);
list.push(type_decl);
});
[...typenames.values()]
.filter(list => list.length > 1)
.forEach(list => {
problems = [...problems, ...list.map(type_decl => new ParseProblem(type_decl.name, `Duplicate type: ${type_decl.qualifiedDottedName()}`, ProblemSeverity.Error))];
});
}
const diagnostics = problems
.filter((p) => p)
.map((p) => {
const start = parsed.positionAt(p.startIdx);
const end = parsed.positionAt(p.endIdx);
/** @type {Diagnostic} */
let diagnostic = {
severity: p.severity,
range: {
start,
end,
},
message: p.message,
source: 'java-android',
};
return diagnostic;
});
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
async function validateTextDocument2(textDocument) {
// In this simple example we get the settings for every validate run.
//let settings = await getDocumentSettings(textDocument.uri);
// The validator creates diagnostics for all uppercase words length 2 and more
let text = textDocument.getText();
let pattern = /\b[A-Z]{2,}\b/g;
let m;
let problems = 0;
let diagnostics = [];
while ((m = pattern.exec(text)) /* && problems < settings.maxNumberOfProblems */) {
problems++;
/** @type {Diagnostic} */
let diagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: textDocument.positionAt(m.index),
end: textDocument.positionAt(m.index + m[0].length)
},
message: `${m[0]} is all uppercase.`,
source: 'ex'
};
if (hasDiagnosticRelatedInformationCapability) {
diagnostic.relatedInformation = [
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range)
problems++;
/** @type {Diagnostic} */
let diagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: textDocument.positionAt(m.index),
end: textDocument.positionAt(m.index + m[0].length),
},
message: 'Spelling matters'
},
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range)
},
message: 'Particularly for names'
}
];
}
diagnostics.push(diagnostic);
message: `${m[0]} is all uppercase.`,
source: 'ex',
};
if (hasDiagnosticRelatedInformationCapability) {
diagnostic.relatedInformation = [
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range),
},
message: 'Spelling matters',
},
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range),
},
message: 'Particularly for names',
},
];
}
diagnostics.push(diagnostic);
}
// Send the computed diagnostics to VS Code.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
connection.onDidChangeWatchedFiles(_change => {
}
connection.onDidChangeWatchedFiles((_change) => {
// Monitored files have change in VS Code
connection.console.log('We received a file change event');
});
// This handler provides the initial list of the completion items.
let allCompletionTypes = null;
connection.onCompletion(
/**
* @param {*} _textDocumentPosition TextDocumentPositionParams
*/
});
// This handler provides the initial list of the completion items.
let allCompletionTypes = null;
connection.onCompletion(
/**
* @param {*} _textDocumentPosition TextDocumentPositionParams
*/
(_textDocumentPosition) => {
// The pass parameter contains the position of the text document in
// which code complete got requested. For the example we ignore this
// info and always provide the same completion items.
const lib = androidLibrary;
if (!lib) return [];
const typeKindMap = {
'class':CompletionItemKind.Class,
'interface': CompletionItemKind.Interface,
'@interface': CompletionItemKind.Interface,
'enum': CompletionItemKind.Enum,
// The pass parameter contains the position of the text document in
// which code complete got requested. For the example we ignore this
// info and always provide the same completion items.
const lib = androidLibrary;
if (!lib) return [];
const typeKindMap = {
class: CompletionItemKind.Class,
interface: CompletionItemKind.Interface,
'@interface': CompletionItemKind.Interface,
enum: CompletionItemKind.Enum,
};
return allCompletionTypes || (allCompletionTypes = lib.types.map((t,idx) =>
/** @type {CompletionItem} */
({
label: t.dottedRawName,
kind: typeKindMap[t.typeKind],
data: idx
})
));
return [
{
label: 'TypeScript',
kind: CompletionItemKind.Text,
data: 1
},
{
label: 'JavaScript',
kind: CompletionItemKind.Text,
data: 2
}
];
return (
allCompletionTypes ||
(allCompletionTypes = [
...'boolean byte char double float int long short void'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
data: -1,
})),
...'public private protected static final abstract volatile native'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
data: -1,
})),
...'false true null'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Value,
data: -1,
})),
...lib.types.map(
(t, idx) =>
/** @type {CompletionItem} */
({
label: t.dottedRawName,
kind: typeKindMap[t.typeKind],
data: idx,
})
),
])
);
}
);
// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
);
// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
/**
* @param {CompletionItem} item
*/
(item) => {
const t = androidLibrary.types[item.data];
item.detail = `${t.package}.${t.dottedRawName}`;
item.documentation = t.docs && {
kind: "markdown",
value: `${t.typeKind} **${t.dottedName}**\n\n${
t.docs
.replace(/(<p ?.*?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,p,i,b,tt,c,lc,li,a,h) => {
return 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))} `
: '';
})
const t = androidLibrary.types[item.data];
if (!t) {
return item;
}
item.detail = `${t.package}.${t.dottedRawName}`;
item.documentation = t.docs && {
kind: 'markdown',
value: `${t.typeKind} **${t.dottedName}**\n\n${
t.docs
.replace(/(<p ?.*?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,p,i,b,tt,c,lc,li,a,h) => {
return 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))} `
: '';
})
}`,
}
return item;
};
return item;
}
);
/*
connection.onDidOpenTextDocument((params) => {
// A text document got opened in VS Code.
// params.uri uniquely identifies the document. For documents store on disk this is a file URI.
// params.text the initial full content of the document.
connection.console.log(`${params.textDocument.uri} opened.`);
});
connection.onDidChangeTextDocument((params) => {
// The content of a text document did change in VS Code.
// params.uri uniquely identifies the document.
// params.contentChanges describe the content changes to the document.
connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`);
});
connection.onDidCloseTextDocument((params) => {
// A text document got closed in VS Code.
// params.uri uniquely identifies the document.
connection.console.log(`${params.textDocument.uri} closed.`);
});
*/
// 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();
);
/*
connection.onDidOpenTextDocument((params) => {
// A text document got opened in VS Code.
// params.uri uniquely identifies the document. For documents store on disk this is a file URI.
// params.text the initial full content of the document.
connection.console.log(`${params.textDocument.uri} opened.`);
});
connection.onDidChangeTextDocument((params) => {
// The content of a text document did change in VS Code.
// params.uri uniquely identifies the document.
// params.contentChanges describe the content changes to the document.
connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`);
});
connection.onDidCloseTextDocument((params) => {
// A text document got closed in VS Code.
// params.uri uniquely identifies the document.
connection.console.log(`${params.textDocument.uri} closed.`);
});
*/
// 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();