els-addon-typed-templates
Version:
Ember Language Server Typed Templates
266 lines (237 loc) • 9.43 kB
text/typescript
import { URI } from "vscode-uri";
import * as ts from "typescript";
import {
Location,
Range,
DiagnosticSeverity,
Diagnostic
} from "vscode-languageserver";
import * as fs from "fs";
import * as path from "path";
import { itemKind } from './utils';
import { serializeArgumentName } from './ast-helpers';
export function normalizeDefinitions(results, root: string) {
return (results || [])
.map(el => {
return tsDefinitionToLocation(el, root);
}).filter((el) => el !== null);
}
const ignoreNames = ['willDestroy', 'toString'];
export function normalizeCompletions(tsResults, realPath, isArg) {
return (tsResults ? tsResults.entries : [])
.filter(({ name }) => !ignoreNames.includes(name) && !name.startsWith("_t") && !name.includes(' - ') && name !== 'globalScope' && name !== 'defaultYield')
.map(el => {
return {
label: isArg
? serializeArgumentName(realPath) + el.name
: realPath + el.name,
data: el.name,
kind: itemKind(el.kind)
};
});
// .map(el => {
// let fixedLabelParts = el.label.split('.');
// fixedLabelParts[fixedLabelParts.length - 1] = el.data;
// return {
// kind: el.kind,
// label: fixedLabelParts.join('.')
// }
// });
}
export function offsetToRange(start, limit, source) {
let rLines = /(.*?(?:\r\n?|\n|$))/gm;
let startLine = source.slice(0, start).match(rLines) || [];
if (!source || startLine.length < 2) {
return Range.create(0, 0, 0, 0);
}
let line = startLine.length - 2;
let col = startLine[startLine.length - 2].length;
let endLine = source.slice(start, limit).match(rLines) || [];
let endCol = col;
let endLineNumber = line;
if (endLine.length === 1) {
endCol = col + limit;
endLineNumber = line + endLine.length - 1;
} else {
endCol = endLine[endLine.length - 1].length;
}
return Range.create(line, col, endLineNumber, endCol);
}
export function tsDefinitionToLocation(el, root) {
let scope = el.textSpan;
let fullPath = path.resolve(el.fileName);
if (!fs.existsSync(fullPath)) {
fullPath = path.resolve(path.join(root, el.fileName));
if (!fs.existsSync(fullPath)) {
return null;
}
}
let file = fs.readFileSync(fullPath, "utf8");
return Location.create(
URI.file(fullPath).toString(),
offsetToRange(scope.start, scope.length, file)
);
}
function messageConverter(msg) {
if (msg.startsWith("The 'this' context of type 'this' is not assignable to method's 'this' of type 'null'.")) {
return "Unable to find context, is file created and property defined?"
}
console.log(msg);
return msg;
}
function getSeverity(msg: string): DiagnosticSeverity {
let severity: DiagnosticSeverity = DiagnosticSeverity.Error;
if (msg.startsWith("Object is possibly 'undefined'")) {
severity = DiagnosticSeverity.Warning;
} else if (msg.startsWith("Object is possibly 'null'")) {
severity = DiagnosticSeverity.Warning;
}
return severity;
}
function toFullDiagnostic(err: ts.Diagnostic) {
if (!err.file || err.start === undefined) {
return null;
}
let preErrorText = err.file.text.slice(0, err.start);
let postErrorText = err.file.text.slice(err.start, err.file.text.length);
// try {
// console.log('err.file.fileName', err.file.fileName);
// console.log('start', err.start);
// console.log('err.slice', err.file.text.slice(err.start, 100));
// console.log('err.code', err.code);
// console.log('err.category', err.category);
// console.log('err.related', err.relatedInformation);
// console.log('err.source', err.source);
// console.log('err.msg', err.messageText);
// } catch(e) {
// console.log('err:', e);
// }
if (err.start < err.file.text.indexOf('@mark-meaningful-issues-start')) {
return null;
}
let closestLeftMark = postErrorText.indexOf('["');
let closestRightMarkOffset = postErrorText.indexOf('"]');
let maybeMark = err.file.text.slice(closestLeftMark + err.start, closestRightMarkOffset + err.start);
let hasNewline = err.file.text.slice(err.start, err.start + closestLeftMark).split('\n').length > 1;
maybeMark = maybeMark.slice(maybeMark.indexOf('[') + 2, maybeMark.indexOf(']')).trim().split(' - ')[0];
let start, end;
if (maybeMark.includes(':') && !hasNewline) {
[start, end] = maybeMark.split(':');
} else {
let preError = preErrorText.slice(preErrorText.lastIndexOf('//@mark'), preErrorText.length);
let mark = preError.slice(preError.indexOf('[') + 1, preError.indexOf(']')).trim();
[start, end] = mark.split(':');
}
if (! start || ! end) {
let postError = err.file.text.slice(err.start, err.file.text.length);
let postErrorMark = postError.slice(postError.indexOf('/*@path-mark ') + 13,postError.indexOf('*/'));
[start, end] = postErrorMark.split(':');
if (!start || ! end) {
console.log(err);
return null;
}
}
// console.log({mark, start, end})
// console.log('preErrorText',preErrorText.slice(preErrorText.lastIndexOf('//@mark ') + 8, preErrorText.lastIndexOf('//@mark ') + 40));
let [startCol, startRow] = start.split(',').map((e)=>parseInt(e, 10));
let [endCol, endRow] = end.split(',').map((e)=>parseInt(e, 10));
let msgText = diagnosticToString(err.messageText);
/*
since ember components in addons may be like
... export default Ember.Component.extend(Base, PromiseResolver, {
it's really tricky to get typings for it at all, and I prefer to skip warnings for it in next lines
*/
if (msgText.startsWith("Object is of type 'unknown'")) {
return null;
}
if (msgText.startsWith("Type 'any' is not assignable to type 'never'")) {
return null;
}
if (msgText.startsWith("Property 'args' does not exist on type")) {
return null;
}
if (msgText.startsWith("Expected 0 arguments, but got 2.")) {
return null;
}
if (msgText.startsWith("Cannot invoke an object which is possibly")) {
return null;
}
return {
severity: getSeverity(msgText),
range: Range.create(startCol - 1, startRow, endCol - 1, endRow),
message: messageConverter(msgText),
source: "typed-templates"
};
}
// regards to https://github.com/dfreeman/ember-typed-templates-vscode/blob/master/src/server/server.ts#L172
function diagnosticToString(message: string | ts.DiagnosticMessageChain, indent = ''): string {
if (typeof message === 'string') {
return `${indent}${message}`;
} else if (message.next && message.next.length) {
let items = message.next.map((msg)=>diagnosticToString(msg, `${indent} `))
return `${indent}${message.messageText}\n${items.join('\n')}`;
} else {
return `${indent}${message.messageText}`;
}
}
export function getFullSemanticDiagnostics(service: ts.LanguageService, fileName) {
const tsDiagnostics = service.getSemanticDiagnostics(fileName);
const results = tsDiagnostics.map((error: any) =>
toFullDiagnostic(error)
).filter((el)=>el !== null);
const diagnostics: Diagnostic[] = results as Diagnostic[];
return diagnostics;
}
export function getSemanticDiagnostics(server, service, templateRange, fileName , focusPath, uri) {
// console.log(service.getSyntacticDiagnostics(fileName).map((el)=>{
// console.log('getSyntacticDiagnostics', el.messageText, el.start, el.length);
// }));
// console.log('getSemanticDiagnostics', fileName);
const tsDiagnostics = service.getSemanticDiagnostics(fileName);
const diagnostics: Diagnostic[] = tsDiagnostics.map((error: any) =>
toDiagnostic(error, templateRange, focusPath)
);
server.connection.sendDiagnostics({ uri, diagnostics });
// console.log(service.getSemanticDiagnostics(fileName).map((el)=>{
// const diagnostics: Diagnostic[] = errors.map((error: any) => toDiagnostic(el));
// server.connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
// console.log('getSemanticDiagnostics', el.messageText, el.start, el.length);
// }));
// console.log(service.getSuggestionDiagnostics(fileName).map((el)=>{
// console.log('getSuggestionDiagnostics', el.messageText, el.start, el.length);
// }));
// console.log('getCompilerOptionsDiagnostics', service.getCompilerOptionsDiagnostics());
}
export function toDiagnostic(
err,
[startIndex, endIndex],
focusPath
): Diagnostic {
let errText = err.file.text.slice(err.start, err.start + err.length);
if (
(err.start >= startIndex && err.length + err.start <= endIndex) ||
errText.startsWith("return ")
) {
let loc = focusPath.node.loc;
return {
severity: getSeverity(err.messageText),
range: loc
? Range.create(
loc.start.line - 1,
loc.start.column,
loc.end.line - 1,
loc.end.column
)
: Range.create(0, 0, 0, 0),
message: messageConverter(err.messageText),
source: "typed-templates"
};
} else {
return {
severity: getSeverity(err.messageText),
range: offsetToRange(0, 0, ""),
message: messageConverter(err.messageText),
source: "typed-templates"
};
}
}