Skip to content

Commit

Permalink
(feat) better error diagnostics (#240)
Browse files Browse the repository at this point in the history
* (feat) better error diagnostics

Narrow down in which step the error occurs and try to extract better error messages.

#86, #232, #129

* (feat) show svelte.config.js errors

* cleanup

* tests

* fix error msg
  • Loading branch information
dummdidumm committed Jun 29, 2020
1 parent d1baf00 commit a9a0ad3
Show file tree
Hide file tree
Showing 4 changed files with 569 additions and 137 deletions.
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 {};
}
}

0 comments on commit a9a0ad3

Please sign in to comment.