Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import ts from 'typescript';
import { CancellationToken, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import {
Document,
mapObjWithRangeToOriginal,
getTextInRange,
isRangeInTag
} from '../../../lib/documents';
import { Document, getTextInRange, isRangeInTag, mapRangeToOriginal } from '../../../lib/documents';
import { DiagnosticsProvider } from '../../interfaces';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange, getDiagnosticTag, mapSeverity } from '../utils';
import { SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { isInGeneratedCode } from './utils';
import { swapRangeStartEndIfNecessary } from '../../../utils';
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { isInGeneratedCode, isAfterSvelte2TsxPropsReturn } from './utils';
import { regexIndexOf, swapRangeStartEndIfNecessary } from '../../../utils';

export class DiagnosticsProviderImpl implements DiagnosticsProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
Expand Down Expand Up @@ -62,7 +57,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
code: diagnostic.code,
tags: getDiagnosticTag(diagnostic)
}))
.map((diagnostic) => mapObjWithRangeToOriginal(fragment, diagnostic))
.map(mapRange(fragment, document))
.filter(hasNoNegativeLines)
.filter(isNoFalsePositive(document, tsDoc))
.map(enhanceIfNecessary)
Expand All @@ -74,6 +69,42 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
}
}

function mapRange(
fragment: SvelteSnapshotFragment,
document: Document
): (value: Diagnostic) => Diagnostic {
return (diagnostic) => {
let range = mapRangeToOriginal(fragment, diagnostic.range);

if (range.start.line < 0) {
const is$$PropsError =
isAfterSvelte2TsxPropsReturn(
fragment.text,
fragment.offsetAt(diagnostic.range.start)
) && diagnostic.message.includes('$$Props');

if (is$$PropsError) {
const propsStart = regexIndexOf(
document.getText(),
/(interface|type)\s+\$\$Props[\s{=]/
);

if (propsStart) {
const start = document.positionAt(
propsStart + document.getText().substring(propsStart).indexOf('$$Props')
);
range = {
start,
end: { ...start, character: start.character + '$$Props'.length }
};
}
}
}

return { ...diagnostic, range };
};
}

/**
* In some rare cases mapping of diagnostics does not work and produces negative lines.
* We filter out these diagnostics with negative lines because else the LSP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { convertRange } from '../utils';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import ts from 'typescript';
import { uniqWith, isEqual } from 'lodash';
import { isComponentAtPosition, isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
import {
isComponentAtPosition,
isAfterSvelte2TsxPropsReturn,
isNoTextSpanInGeneratedCode,
SnapshotFragmentMap
} from './utils';

export class RenameProviderImpl implements RenameProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
Expand Down Expand Up @@ -273,11 +278,7 @@ export class RenameProviderImpl implements RenameProvider {

// --------> svelte2tsx?
private isInSvelte2TsxPropLine(fragment: SvelteSnapshotFragment, loc: ts.RenameLocation) {
const textBeforeProp = fragment.text.substring(0, loc.textSpan.start);
// This is how svelte2tsx writes out the props
if (textBeforeProp.includes('\nreturn { props: {')) {
return true;
}
return isAfterSvelte2TsxPropsReturn(fragment.text, loc.textSpan.start);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,11 @@ export class SnapshotFragmentMap {
return (await this.retrieve(fileName)).fragment;
}
}

export function isAfterSvelte2TsxPropsReturn(text: string, end: number) {
const textBeforeProp = text.substring(0, end);
// This is how svelte2tsx writes out the props
if (textBeforeProp.includes('\nreturn { props: {')) {
return true;
}
}
13 changes: 13 additions & 0 deletions packages/language-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ export function regexLastIndexOf(text: string, regex: RegExp, endPos?: number) {
return lastIndexOf;
}

/**
* Like str.indexOf, but for regular expressions.
*/
export function regexIndexOf(text: string, regex: RegExp, startPos?: number) {
if (startPos === undefined || startPos < 0) {
startPos = 0;
}

const stringToWorkWith = text.substring(startPos);
const result: RegExpExecArray | null = regex.exec(stringToWorkWith);
return result?.index ?? -1;
}

/**
* Get all matches of a regexp.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,12 @@ describe('DiagnosticsProvider', () => {
]);
});

it('filters out unused $$Generic hint', async () => {
const { plugin, document } = setup('$$generic-unused.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, []);
});

it('checks $$Events usage', async () => {
const { plugin, document } = setup('$$events.svelte');
const diagnostics = await plugin.getDiagnostics(document);
Expand Down Expand Up @@ -1582,4 +1588,169 @@ describe('DiagnosticsProvider', () => {
}
]);
});

it('checks $$Props usage (valid)', async () => {
const { plugin, document } = setup('$$props-valid.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, []);
});

it('checks $$Props usage (invalid1)', async () => {
const { plugin, document } = setup('$$props-invalid1.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, [
{
code: 2345,
message:
// eslint-disable-next-line max-len
"Argument of type '$$Props' is not assignable to parameter of type '{ exported1: string; }'.\n Types of property 'exported1' are incompatible.\n Type 'string | undefined' is not assignable to type 'string'.\n Type 'undefined' is not assignable to type 'string'.",
range: {
end: {
character: 18,
line: 1
},
start: {
character: 11,
line: 1
}
},
severity: 1,
source: 'ts',
tags: []
}
]);
});

it('checks $$Props usage (invalid2)', async () => {
const { plugin, document } = setup('$$props-invalid2.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, [
{
code: 2345,
message:
// eslint-disable-next-line max-len
"Argument of type '$$Props' is not assignable to parameter of type '{ exported1?: string | undefined; }'.\n Types of property 'exported1' are incompatible.\n Type 'boolean' is not assignable to type 'string | undefined'.",
range: {
end: {
character: 18,
line: 1
},
start: {
character: 11,
line: 1
}
},
severity: 1,
source: 'ts',
tags: []
}
]);
});

it('checks $$Props usage (invalid3)', async () => {
const { plugin, document } = setup('$$props-invalid3.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, [
{
code: 2345,
message:
// eslint-disable-next-line max-len
"Argument of type '$$Props' is not assignable to parameter of type '{ wrong: boolean; }'.\n Property 'wrong' is missing in type '$$Props' but required in type '{ wrong: boolean; }'.",
range: {
end: {
character: 18,
line: 1
},
start: {
character: 11,
line: 1
}
},
severity: 1,
source: 'ts',
tags: []
},
{
code: 2345,
message:
// eslint-disable-next-line max-len
"Argument of type '{ wrong: boolean; }' is not assignable to parameter of type 'Partial<$$Props>'.\n Object literal may only specify known properties, and 'wrong' does not exist in type 'Partial<$$Props>'.",
range: {
end: {
character: 18,
line: 1
},
start: {
character: 11,
line: 1
}
},
severity: 1,
source: 'ts',
tags: []
}
]);
});

it('checks $$Props component usage', async () => {
const { plugin, document } = setup('using-$$props.svelte');
const diagnostics = await plugin.getDiagnostics(document);
assert.deepStrictEqual(diagnostics, [
{
code: 2322,
message: "Type 'boolean' is not assignable to type 'string'.",
range: {
end: {
character: 16,
line: 9
},
start: {
character: 7,
line: 9
}
},
severity: 1,
source: 'ts',
tags: []
},
{
code: 2322,
message:
// eslint-disable-next-line max-len
"Type '{ exported1: string; exported2: string; invalidProp: boolean; }' is not assignable to type 'IntrinsicAttributes & { exported1: string; exported2?: string | undefined; }'.\n Property 'invalidProp' does not exist on type 'IntrinsicAttributes & { exported1: string; exported2?: string | undefined; }'.",
range: {
end: {
character: 54,
line: 10
},
start: {
character: 43,
line: 10
}
},
severity: 1,
source: 'ts',
tags: []
},
{
code: 2322,
message:
// eslint-disable-next-line max-len
"Type '{}' is not assignable to type 'IntrinsicAttributes & { exported1: string; exported2?: string | undefined; }'.\n Property 'exported1' is missing in type '{}' but required in type '{ exported1: string; exported2?: string | undefined; }'.",
range: {
end: {
character: 6,
line: 11
},
start: {
character: 1,
line: 11
}
},
severity: 1,
source: 'ts',
tags: []
}
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
// The "is unused" hint will appear at the script tag
// So we rather filter out the hint completely
type T = $$Generic;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
interface $$Props {
exported1?: string;
}

export let exported1: string;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
interface $$Props {
exported1: boolean;
}

export let exported1 = 'wrong type'
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
interface $$Props {
exported1: boolean;
}

export let wrong: boolean;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
interface $$Props {
exported1: string;
exported2?: string;
}

export let exported1: string;
export let exported2 = '';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import Props from './$$props-valid.svelte';
import PropsInvalid3 from './$$props-invalid3.svelte';
</script>

<!-- valid -->
<Props exported1="valid" exported2="valid" />
<PropsInvalid3 exported1={true} />
<!-- invalid -->
<Props exported1={true} exported2="valid" />
<Props exported1="valid" exported2="valid" invalidProp={true} />
<Props />
Loading