Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) better error diagnostics #240

Merged
merged 5 commits into from Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 28 additions & 9 deletions packages/language-server/src/plugins/svelte/SvelteDocument.ts
Expand Up @@ -21,6 +21,12 @@ export type SvelteCompileResult = ReturnType<typeof compile>;

export interface SvelteConfig extends CompileOptions {
preprocess?: PreprocessorGroup;
loadConfigError?: any;
}

export enum TranspileErrorSource {
Script = 'Script',
Style = 'Style',
}

/**
Expand Down Expand Up @@ -78,7 +84,10 @@ export class SvelteDocument {

private getCompileOptions() {
const config = { ...this.config };
delete config.preprocess; // svelte compiler throws an error if we don't do this
// svelte compiler throws an error if we don't do this
delete config.preprocess;
delete config.loadConfigError;

return config;
}

Expand Down Expand Up @@ -272,21 +281,31 @@ async function transpile(document: Document, preprocessors: PreprocessorGroup =

if (preprocessors.script) {
preprocessor.script = async (args: any) => {
const res = await preprocessors.script!(args);
if (res && res.map) {
processedScript = res;
try {
const res = await preprocessors.script!(args);
if (res && res.map) {
processedScript = res;
}
return res;
} catch (e) {
e.__source = TranspileErrorSource.Script;
throw e;
}
return res;
};
}

if (preprocessors.style) {
preprocessor.style = async (args: any) => {
const res = await preprocessors.style!(args);
if (res && res.map) {
processedStyle = res;
try {
const res = await preprocessors.style!(args);
if (res && res.map) {
processedStyle = res;
}
return res;
} catch (e) {
e.__source = TranspileErrorSource.Style;
throw e;
}
return res;
};
}

Expand Down
165 changes: 37 additions & 128 deletions packages/language-server/src/plugins/svelte/SveltePlugin.ts
@@ -1,31 +1,31 @@
import { cosmiconfig } from 'cosmiconfig';
import { CompileOptions, Warning } from 'svelte/types/compiler/interfaces';
import { CompileOptions } from 'svelte/types/compiler/interfaces';
import {
CodeAction,
CodeActionContext,
CompletionList,
Diagnostic,
DiagnosticSeverity,
Hover,
Position,
Range,
TextEdit,
CodeActionContext,
CodeAction,
} from 'vscode-languageserver';
import { Document, isInTag, mapDiagnosticToOriginal } from '../../lib/documents';
import { Document } from '../../lib/documents';
import { Logger } from '../../logger';
import { LSConfigManager, LSSvelteConfig } from '../../ls-config';
import { importPrettier, importSveltePreprocess } from '../importPackage';
import {
CodeActionsProvider,
CompletionsProvider,
DiagnosticsProvider,
FormattingProvider,
HoverProvider,
CodeActionsProvider,
} from '../interfaces';
import { getCodeActions } from './features/getCodeActions';
import { getCompletions } from './features/getCompletions';
import { getDiagnostics } from './features/getDiagnostics';
import { getHoverInfo } from './features/getHoverInfo';
import { SvelteDocument, SvelteConfig, SvelteCompileResult } from './SvelteDocument';
import { Logger } from '../../logger';
import { getCodeActions } from './features/getCodeActions';
import { SvelteCompileResult, SvelteConfig, SvelteDocument } from './SvelteDocument';

const DEFAULT_OPTIONS: CompileOptions = {
dev: true,
Expand All @@ -34,10 +34,6 @@ const DEFAULT_OPTIONS: CompileOptions = {
const NO_GENERATE: CompileOptions = {
generate: false,
};

const scssNodeRuntimeHint =
'If you use SCSS, it may be necessary to add the path to your NODE runtime to the setting `svelte.language-server.runtime`, or use `sass` instead of `node-sass`. ';

export class SveltePlugin
implements
DiagnosticsProvider,
Expand All @@ -58,63 +54,7 @@ export class SveltePlugin
return [];
}

try {
return await this.tryGetDiagnostics(document);
} catch (error) {
Logger.error('Preprocessing failed');
Logger.error(error);
// Preprocessing could fail if packages like less/sass/babel cannot be resolved
// when our fallback-version of svelte-preprocess is used.
// Add a warning about a broken svelte.configs.js/preprocessor setup
// Also add svelte-preprocess error message.
const errorMsg =
error instanceof Error && error.message.startsWith('Cannot find any of modules')
? error.message + '. '
: '';
const hint =
error instanceof Error && error.message.includes('node-sass')
? scssNodeRuntimeHint
: '';
return [
{
message:
errorMsg +
"The file cannot be parsed because script or style require a preprocessor that doesn't seem to be setup. " +
'Did you setup a `svelte.config.js`? ' +
hint +
'See https://github.com/sveltejs/language-tools/tree/master/packages/svelte-vscode#using-with-preprocessors for more info.',
range: Range.create(Position.create(0, 0), Position.create(0, 5)),
severity: DiagnosticSeverity.Warning,
source: 'svelte',
},
];
}
}

private async tryGetDiagnostics(document: Document): Promise<Diagnostic[]> {
const svelteDoc = await this.getSvelteDoc(document);
const transpiled = await svelteDoc.getTranspiled();

try {
const res = await svelteDoc.getCompiled();
return (((res.stats as any).warnings || res.warnings || []) as Warning[])
.map((warning) => {
const start = warning.start || { line: 1, column: 0 };
const end = warning.end || start;
return {
range: Range.create(start.line - 1, start.column, end.line - 1, end.column),
message: warning.message,
severity: DiagnosticSeverity.Warning,
source: 'svelte',
code: warning.code,
};
})
.map((diag) => mapDiagnosticToOriginal(transpiled, diag));
} catch (err) {
return (await this.createParserErrorDiagnostic(err, document)).map((diag) =>
mapDiagnosticToOriginal(transpiled, diag),
);
}
return getDiagnostics(document, await this.getSvelteDoc(document));
}

async getCompiledResult(document: Document): Promise<SvelteCompileResult | null> {
Expand All @@ -126,63 +66,6 @@ export class SveltePlugin
}
}

private async createParserErrorDiagnostic(error: any, document: Document) {
const start = error.start || { line: 1, column: 0 };
const end = error.end || start;
const diagnostic: Diagnostic = {
range: Range.create(start.line - 1, start.column, end.line - 1, end.column),
message: error.message,
severity: DiagnosticSeverity.Error,
source: 'svelte',
code: error.code,
};

if (diagnostic.message.includes('expected')) {
const isInStyle = isInTag(diagnostic.range.start, document.styleInfo);
const isInScript = isInTag(diagnostic.range.start, document.scriptInfo);

if (isInStyle || isInScript) {
diagnostic.message +=
'. If you expect this syntax to work, here are some suggestions: ';
if (isInScript) {
diagnostic.message +=
'If you use typescript with `svelte-preprocessor`, did you add `lang="typescript"` to your `script` tag? ';
} else {
diagnostic.message +=
'If you use less/SCSS with `svelte-preprocessor`, did you add `lang="scss"`/`lang="less"` to you `style` tag? ' +
scssNodeRuntimeHint;
}
diagnostic.message +=
'Did you setup a `svelte.config.js`? ' +
'See https://github.com/sveltejs/language-tools/tree/master/packages/svelte-vscode#using-with-preprocessors for more info.';
}
}

return [diagnostic];
}

private useFallbackPreprocessor(document: Document, foundConfig: boolean) {
if (
document.styleInfo?.attributes.lang ||
document.styleInfo?.attributes.type ||
document.scriptInfo?.attributes.lang ||
document.scriptInfo?.attributes.type
) {
Logger.log(
(foundConfig
? 'Found svelte.config.js but there was an error loading it. '
: 'No svelte.config.js found but one is needed. ') +
'Using https://github.com/sveltejs/svelte-preprocess as fallback',
);
return {
preprocess: importSveltePreprocess(document.getFilePath() || '')({
typescript: { transpileOnly: true },
}),
};
}
return {};
}

async formatDocument(document: Document): Promise<TextEdit[]> {
if (!this.featureEnabled('format')) {
return [];
Expand Down Expand Up @@ -251,7 +134,10 @@ export class SveltePlugin
if (!svelteDoc || svelteDoc.version !== document.version) {
svelteDoc?.destroyTranspiled();
// Reuse previous config. Assumption: Config does not change often (if at all).
const config = svelteDoc?.config || (await this.loadConfig(document));
const config =
svelteDoc?.config && !svelteDoc.config.loadConfigError
? svelteDoc.config
: await this.loadConfig(document);
svelteDoc = new SvelteDocument(document, config);
this.docManager.set(document, svelteDoc);
}
Expand All @@ -274,7 +160,30 @@ export class SveltePlugin
...DEFAULT_OPTIONS,
...this.useFallbackPreprocessor(document, true),
...NO_GENERATE,
loadConfigError: err,
};
}
}

private useFallbackPreprocessor(document: Document, foundConfig: boolean) {
if (
document.styleInfo?.attributes.lang ||
document.styleInfo?.attributes.type ||
document.scriptInfo?.attributes.lang ||
document.scriptInfo?.attributes.type
) {
Logger.log(
(foundConfig
? 'Found svelte.config.js but there was an error loading it. '
: 'No svelte.config.js found but one is needed. ') +
'Using https://github.com/sveltejs/svelte-preprocess as fallback',
);
return {
preprocess: importSveltePreprocess(document.getFilePath() || '')({
typescript: { transpileOnly: true },
}),
};
}
return {};
}
}