diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts index 0a01a7970..90ee9d4a5 100644 --- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts +++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts @@ -57,6 +57,10 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot { * in order to prevent memory leaks. */ destroyFragment(): void; + /** + * Convenience function for getText(0, getLength()) + */ + getFullText(): string; } /** @@ -221,6 +225,10 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot { return this.text.length; } + getFullText() { + return this.text; + } + getChangeRange() { return undefined; } @@ -301,6 +309,10 @@ export class JSOrTSDocumentSnapshot return this.text.length; } + getFullText() { + return this.text; + } + getChangeRange() { return undefined; } diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 5831e91e4..fb66043b4 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -28,7 +28,7 @@ import { getTextInRange } from '../../lib/documents'; import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; -import { pathToUrl } from '../../utils'; +import { isNotNullOrUndefined, pathToUrl } from '../../utils'; import { AppCompletionItem, AppCompletionList, @@ -49,7 +49,6 @@ import { SemanticTokensProvider, UpdateTsOrJsFile } from '../interfaces'; -import { SnapshotFragment } from './DocumentSnapshot'; import { CodeActionsProviderImpl } from './features/CodeActionsProvider'; import { CompletionEntryWithIdentifer, @@ -67,6 +66,7 @@ import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider'; import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider'; import { SnapshotManager } from './SnapshotManager'; import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider'; +import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils'; export class TypeScriptPlugin implements @@ -263,35 +263,35 @@ export class TypeScriptPlugin } const { lang, tsDoc } = this.getLSAndTSDoc(document); - const fragment = await tsDoc.getFragment(); + const mainFragment = await tsDoc.getFragment(); const defs = lang.getDefinitionAndBoundSpan( tsDoc.filePath, - fragment.offsetAt(fragment.getGeneratedPosition(position)) + mainFragment.offsetAt(mainFragment.getGeneratedPosition(position)) ); if (!defs || !defs.definitions) { return []; } - const docs = new Map([[tsDoc.filePath, fragment]]); + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); + docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc }); - return await Promise.all( + const result = await Promise.all( defs.definitions.map(async (def) => { - let defDoc = docs.get(def.fileName); - if (!defDoc) { - defDoc = await this.getSnapshot(def.fileName).getFragment(); - docs.set(def.fileName, defDoc); + const { fragment, snapshot } = await docs.retrieve(def.fileName); + + if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) { + return LocationLink.create( + pathToUrl(def.fileName), + convertToLocationRange(fragment, def.textSpan), + convertToLocationRange(fragment, def.textSpan), + convertToLocationRange(mainFragment, defs.textSpan) + ); } - - return LocationLink.create( - pathToUrl(def.fileName), - convertToLocationRange(defDoc, def.textSpan), - convertToLocationRange(defDoc, def.textSpan), - convertToLocationRange(fragment, defs.textSpan) - ); }) ); + return result.filter(isNotNullOrUndefined); } async prepareRename(document: Document, position: Position): Promise { @@ -436,10 +436,6 @@ export class TypeScriptPlugin return this.lsAndTsDocResolver.getLSAndTSDoc(document); } - private getSnapshot(filePath: string, document?: Document) { - return this.lsAndTsDocResolver.getSnapshot(filePath, document); - } - /** * * @internal diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index b731accc0..fbea0b506 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -9,7 +9,7 @@ import { WorkspaceEdit } from 'vscode-languageserver'; import { Document, mapRangeToOriginal, isRangeInTag, isInTag } from '../../../lib/documents'; -import { pathToUrl, flatten } from '../../../utils'; +import { pathToUrl, flatten, isNotNullOrUndefined } from '../../../utils'; import { CodeActionsProvider } from '../../interfaces'; import { SnapshotFragment, SvelteSnapshotFragment } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; @@ -17,6 +17,7 @@ import { convertRange } from '../utils'; import ts from 'typescript'; import { CompletionsProviderImpl } from './CompletionProvider'; +import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils'; interface RefactorArgs { type: 'refactor'; @@ -134,53 +135,65 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { userPreferences ); - const docs = new Map([[tsDoc.filePath, fragment]]); + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); + docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc }); + return await Promise.all( codeFixes.map(async (fix) => { const documentChanges = await Promise.all( fix.changes.map(async (change) => { - const doc = - docs.get(change.fileName) ?? - (await this.getAndCacheCodeActionDoc(change, docs)); + const { snapshot, fragment } = await docs.retrieve(change.fileName); return TextDocumentEdit.create( VersionedTextDocumentIdentifier.create(pathToUrl(change.fileName), 0), - change.textChanges.map((edit) => { - if ( - fix.fixName === 'import' && - doc instanceof SvelteSnapshotFragment - ) { - return this.completionProvider.codeActionChangeToTextEdit( - document, - doc, - edit, - true, - isInTag(range.start, document.scriptInfo) || - isInTag(range.start, document.moduleScriptInfo) - ); - } - - let originalRange = mapRangeToOriginal( - doc, - convertRange(doc, edit.span) - ); - if (fix.fixName === 'unusedIdentifier') { - originalRange = this.checkRemoveImportCodeActionRange( - edit, - doc, - originalRange + change.textChanges + .map((edit) => { + if ( + fix.fixName === 'import' && + fragment instanceof SvelteSnapshotFragment + ) { + return this.completionProvider.codeActionChangeToTextEdit( + document, + fragment, + edit, + true, + isInTag(range.start, document.scriptInfo) || + isInTag(range.start, document.moduleScriptInfo) + ); + } + + if ( + !isNoTextSpanInGeneratedCode( + snapshot.getFullText(), + edit.span + ) + ) { + return undefined; + } + + let originalRange = mapRangeToOriginal( + fragment, + convertRange(fragment, edit.span) ); - } - if (fix.fixName === 'fixMissingFunctionDeclaration') { - originalRange = this.checkEndOfFileCodeInsert( - originalRange, - range, - document - ); - } - - return TextEdit.replace(originalRange, edit.newText); - }) + if (fix.fixName === 'unusedIdentifier') { + originalRange = this.checkRemoveImportCodeActionRange( + edit, + fragment, + originalRange + ); + } + + if (fix.fixName === 'fixMissingFunctionDeclaration') { + originalRange = this.checkEndOfFileCodeInsert( + originalRange, + range, + document + ); + } + + return TextEdit.replace(originalRange, edit.newText); + }) + .filter(isNotNullOrUndefined) ); }) ); @@ -195,15 +208,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ); } - private async getAndCacheCodeActionDoc( - change: ts.FileTextChanges, - cache: Map - ) { - const doc = await this.getSnapshot(change.fileName).getFragment(); - cache.set(change.fileName, doc); - return doc; - } - private async getApplicableRefactors(document: Document, range: Range): Promise { if ( !isRangeInTag(range, document.scriptInfo) && diff --git a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts index 50194860a..20c6cf274 100644 --- a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts @@ -5,6 +5,7 @@ import { DiagnosticsProvider } from '../../interfaces'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { convertRange, mapSeverity } from '../utils'; import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; +import { isInGeneratedCode } from './utils'; export class DiagnosticsProviderImpl implements DiagnosticsProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} @@ -40,6 +41,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider { const fragment = await tsDoc.getFragment(); return diagnostics + .filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength()))) .map((diagnostic) => ({ range: convertRange(tsDoc, diagnostic), severity: mapSeverity(diagnostic.category), @@ -208,3 +210,16 @@ function swapRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic { } return diag; } + +/** + * Checks if diagnostic is not within a section that should be completely ignored + * because it's purely generated. + */ +function isNotGenerated(text: string) { + return (diagnostic: ts.Diagnostic) => { + if (diagnostic.start === undefined || diagnostic.length === undefined) { + return true; + } + return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length); + }; +} diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts index f90b24e54..21a70c6d7 100644 --- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts @@ -1,10 +1,11 @@ +import ts from 'typescript'; import { Location, Position, ReferenceContext } from 'vscode-languageserver'; import { Document } from '../../../lib/documents'; import { pathToUrl } from '../../../utils'; import { FindReferencesProvider } from '../../interfaces'; -import { SnapshotFragment } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { convertToLocationRange } from '../utils'; +import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils'; export class FindReferencesProviderImpl implements FindReferencesProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} @@ -25,17 +26,15 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { return null; } - const docs = new Map([[tsDoc.filePath, fragment]]); + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); + docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc }); return await Promise.all( references .filter((ref) => context.includeDeclaration || !ref.isDefinition) + .filter(notInGeneratedCode(tsDoc.getFullText())) .map(async (ref) => { - let defDoc = docs.get(ref.fileName); - if (!defDoc) { - defDoc = await this.getSnapshot(ref.fileName).getFragment(); - docs.set(ref.fileName, defDoc); - } + const defDoc = await docs.retrieveFragment(ref.fileName); return Location.create( pathToUrl(ref.fileName), @@ -48,8 +47,10 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { private getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } +} - private getSnapshot(filePath: string, document?: Document) { - return this.lsAndTsDocResolver.getSnapshot(filePath, document); - } +function notInGeneratedCode(text: string) { + return (ref: ts.ReferenceEntry) => { + return isNoTextSpanInGeneratedCode(text, ref.textSpan); + }; } diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 3216b4e3a..0d9175234 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -6,7 +6,7 @@ import { offsetAt, getLineAtPosition } from '../../../lib/documents'; -import { pathToUrl } from '../../../utils'; +import { isNotNullOrUndefined, pathToUrl } from '../../../utils'; import { RenameProvider } from '../../interfaces'; import { SnapshotFragment, @@ -17,6 +17,7 @@ import { convertRange } from '../utils'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import ts from 'typescript'; import { uniqWith, isEqual } from 'lodash'; +import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils'; export class RenameProviderImpl implements RenameProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} @@ -61,7 +62,8 @@ export class RenameProviderImpl implements RenameProvider { return null; } - const docs = new Map([[tsDoc.filePath, fragment]]); + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); + docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc }); let convertedRenameLocations: Array< ts.RenameLocation & { range: Range; @@ -158,7 +160,7 @@ export class RenameProviderImpl implements RenameProvider { fragment: SvelteSnapshotFragment, position: Position, convertedRenameLocations: Array, - fragments: Map, + fragments: SnapshotFragmentMap, lang: ts.LanguageService ) { // First find out if it's really the "rename prop inside component with that prop" case @@ -214,7 +216,7 @@ export class RenameProviderImpl implements RenameProvider { */ private async getAdditionalLocationsForRenameOfPropInsideOtherComponent( convertedRenameLocations: Array, - fragments: Map, + fragments: SnapshotFragmentMap, lang: ts.LanguageService ) { // Check if it's a prop rename @@ -226,7 +228,7 @@ export class RenameProviderImpl implements RenameProvider { return []; } // Find generated `export let` - const doc = fragments.get(updatePropLocation.fileName); + const doc = fragments.getFragment(updatePropLocation.fileName); const match = this.matchGeneratedExportLet(doc, updatePropLocation); if (!match) { return []; @@ -256,7 +258,7 @@ export class RenameProviderImpl implements RenameProvider { private findLocationWhichWantsToUpdatePropName( convertedRenameLocations: Array, - fragments: Map + fragments: SnapshotFragmentMap ) { return convertedRenameLocations.find((loc) => { // Props are not in mapped range @@ -264,7 +266,7 @@ export class RenameProviderImpl implements RenameProvider { return; } - const fragment = fragments.get(loc.fileName); + const fragment = fragments.getFragment(loc.fileName); // Props are in svelte snapshots only if (!(fragment instanceof SvelteSnapshotFragment)) { return false; @@ -295,23 +297,21 @@ export class RenameProviderImpl implements RenameProvider { */ private async mapAndFilterRenameLocations( renameLocations: readonly ts.RenameLocation[], - fragments: Map + fragments: SnapshotFragmentMap ): Promise> { const mappedLocations = await Promise.all( renameLocations.map(async (loc) => { - let doc = fragments.get(loc.fileName); - if (!doc) { - doc = await this.getSnapshot(loc.fileName).getFragment(); - fragments.set(loc.fileName, doc); - } + const { fragment, snapshot } = await fragments.retrieve(loc.fileName); - return { - ...loc, - range: this.mapRangeToOriginal(doc, loc.textSpan) - }; + if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), loc.textSpan)) { + return { + ...loc, + range: this.mapRangeToOriginal(fragment, loc.textSpan) + }; + } }) ); - return this.filterWrongRenameLocations(mappedLocations); + return this.filterWrongRenameLocations(mappedLocations.filter(isNotNullOrUndefined)); } private filterWrongRenameLocations( diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts index e36f8acdf..d473f9d2d 100644 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts @@ -4,12 +4,12 @@ import { VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver'; -import { Document, mapRangeToOriginal } from '../../../lib/documents'; +import { mapRangeToOriginal } from '../../../lib/documents'; import { urlToPath } from '../../../utils'; import { FileRename, UpdateImportsProvider } from '../../interfaces'; -import { SnapshotFragment } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { convertRange } from '../utils'; +import { SnapshotFragmentMap } from './utils'; export class UpdateImportsProviderImpl implements UpdateImportsProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} @@ -37,21 +37,17 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return change; }); - const docs = new Map(); + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); const documentChanges = await Promise.all( updateImportsChanges.map(async (change) => { - let fragment = docs.get(change.fileName); - if (!fragment) { - fragment = await this.getSnapshot(change.fileName).getFragment(); - docs.set(change.fileName, fragment); - } + const fragment = await docs.retrieveFragment(change.fileName); return TextDocumentEdit.create( VersionedTextDocumentIdentifier.create(fragment.getURL(), 0), change.textChanges.map((edit) => { const range = mapRangeToOriginal( - fragment!, - convertRange(fragment!, edit.span) + fragment, + convertRange(fragment, edit.span) ); return TextEdit.replace(range, edit.newText); }) @@ -65,8 +61,4 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { private getLSForPath(path: string) { return this.lsAndTsDocResolver.getLSForPath(path); } - - private getSnapshot(filePath: string, document?: Document) { - return this.lsAndTsDocResolver.getSnapshot(filePath, document); - } } diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index afefc9e60..fc014dd4a 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -1,7 +1,12 @@ import ts from 'typescript'; import { Position } from 'vscode-languageserver'; import { Document, getNodeIfIsInComponentStartTag, isInTag } from '../../../lib/documents'; -import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot'; +import { + DocumentSnapshot, + SnapshotFragment, + SvelteDocumentSnapshot, + SvelteSnapshotFragment +} from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; /** @@ -48,3 +53,56 @@ export function getComponentAtPosition( } return snapshot; } + +/** + * Checks if this a section that should be completely ignored + * because it's purely generated. + */ +export function isInGeneratedCode(text: string, start: number, end: number) { + const lineStart = text.lastIndexOf('\n', start); + const lineEnd = text.indexOf('\n', end); + return ( + text.substring(lineStart, start).includes('/*Ωignore_startΩ*/') && + text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/') + ); +} + +/** + * Checks that this isn't a text span that should be completely ignored + * because it's purely generated. + */ +export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) { + return !isInGeneratedCode(text, span.start, span.start + span.length); +} + +export class SnapshotFragmentMap { + private map = new Map(); + constructor(private resolver: LSAndTSDocResolver) {} + + set(fileName: string, content: { fragment: SnapshotFragment; snapshot: DocumentSnapshot }) { + this.map.set(fileName, content); + } + + get(fileName: string) { + return this.map.get(fileName); + } + + getFragment(fileName: string) { + return this.map.get(fileName)?.fragment; + } + + async retrieve(fileName: string) { + let snapshotFragment = this.get(fileName); + if (!snapshotFragment) { + const snapshot = this.resolver.getSnapshot(fileName); + const fragment = await snapshot.getFragment(); + snapshotFragment = { fragment, snapshot }; + this.set(fileName, snapshotFragment); + } + return snapshotFragment; + } + + async retrieveFragment(fileName: string) { + return (await this.retrieve(fileName)).fragment; + } +} diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts index 6a99ab5c0..a7b131a31 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -549,4 +549,309 @@ describe('DiagnosticsProvider', () => { } ]); }); + + it('if control flow', async () => { + const { plugin, document } = setup('diagnostics-if-control-flow.svelte'); + const diagnostics = await plugin.getDiagnostics(document); + + assert.deepStrictEqual(diagnostics, [ + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 15, + line: 13 + }, + start: { + character: 5, + line: 13 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 19, + line: 16 + }, + start: { + character: 9, + line: 16 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 19, + line: 20 + }, + start: { + character: 9, + line: 20 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2532, + message: "Object is possibly 'undefined'.", + range: { + end: { + character: 14, + line: 33 + }, + start: { + character: 13, + line: 33 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2367, + message: + "This condition will always return 'false' since the types 'boolean' and 'string' have no overlap.", + range: { + end: { + character: 26, + line: 35 + }, + start: { + character: 17, + line: 35 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 25, + line: 44 + }, + start: { + character: 13, + line: 44 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2322, + message: + "Type 'string | boolean' is not assignable to type 'string'.\n Type 'boolean' is not assignable to type 'string'.", + range: { + end: { + character: 8, + line: 53 + }, + start: { + character: 1, + line: 53 + } + }, + severity: 1, + source: 'ts', + tags: [] + } + ]); + }); + + it('if control flow with shadowed variables', async () => { + const { plugin, document } = setup('diagnostics-if-control-flow-shadowed.svelte'); + const diagnostics = await plugin.getDiagnostics(document); + + assert.deepStrictEqual(diagnostics, [ + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 15, + line: 13 + }, + start: { + character: 5, + line: 13 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2322, + message: "Type 'boolean' is not assignable to type 'string'.", + range: { + end: { + character: 16, + line: 17 + }, + start: { + character: 9, + line: 17 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2339, + message: "Property 'a' does not exist on type 'boolean'.", + range: { + end: { + character: 16, + line: 23 + }, + start: { + character: 15, + line: 23 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2339, + message: + "Property 'a' does not exist on type 'string | boolean'.\n Property 'a' does not exist on type 'string'.", + range: { + end: { + character: 16, + line: 29 + }, + start: { + character: 15, + line: 29 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2367, + message: + "This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.", + range: { + end: { + character: 24, + line: 31 + }, + start: { + character: 17, + line: 31 + } + }, + severity: 1, + source: 'ts', + tags: [] + } + ]); + }); + + it('ignores diagnostics in generated code', async () => { + const { plugin, document } = setup('diagnostics-ignore-generated.svelte'); + const diagnostics = await plugin.getDiagnostics(document); + + assert.deepStrictEqual(diagnostics, [ + { + code: 2304, + message: "Cannot find name 'a'.", + range: { + end: { + character: 13, + line: 3 + }, + start: { + character: 12, + line: 3 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2304, + message: "Cannot find name 'a'.", + range: { + end: { + character: 6, + line: 4 + }, + start: { + character: 5, + line: 4 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2304, + message: "Cannot find name 'b'.", + range: { + end: { + character: 10, + line: 8 + }, + start: { + character: 9, + line: 8 + } + }, + severity: 1, + source: 'ts', + tags: [] + }, + { + code: 2304, + message: "Cannot find name 'b'.", + range: { + end: { + character: 10, + line: 9 + }, + start: { + character: 9, + line: 9 + } + }, + severity: 1, + source: 'ts', + tags: [] + } + ]); + }); }); diff --git a/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts b/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts index 44cb68133..a4170c6a7 100644 --- a/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts @@ -156,4 +156,53 @@ describe('FindReferencesProvider', () => { } ]); }); + + it('ignores references inside generated code', async () => { + const { provider, document } = setup('find-references-ignore-generated.svelte'); + + const results = await provider.findReferences(document, Position.create(1, 8), { + includeDeclaration: true + }); + assert.deepStrictEqual(results, [ + { + range: { + end: { + character: 9, + line: 1 + }, + start: { + character: 8, + line: 1 + } + }, + uri: getUri('find-references-ignore-generated.svelte') + }, + { + range: { + end: { + character: 6, + line: 5 + }, + start: { + character: 5, + line: 5 + } + }, + uri: getUri('find-references-ignore-generated.svelte') + }, + { + range: { + end: { + character: 21, + line: 7 + }, + start: { + character: 20, + line: 7 + } + }, + uri: getUri('find-references-ignore-generated.svelte') + } + ]); + }); }); diff --git a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts index 3b811e268..197abb1d8 100644 --- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts @@ -35,6 +35,7 @@ describe('RenameProvider', () => { const renameDoc4 = await openDoc('rename4.svelte'); const renameDoc5 = await openDoc('rename5.svelte'); const renameDoc6 = await openDoc('rename6.svelte'); + const renameDocIgnoreGenerated = await openDoc('rename-ignore-generated.svelte'); return { provider, renameDoc1, @@ -43,6 +44,7 @@ describe('RenameProvider', () => { renameDoc4, renameDoc5, renameDoc6, + renameDocIgnoreGenerated, docManager }; @@ -469,4 +471,59 @@ describe('RenameProvider', () => { } }); }); + + it('should rename and ignore generated', async () => { + const { provider, renameDocIgnoreGenerated } = await setup(); + const result = await provider.rename( + renameDocIgnoreGenerated, + Position.create(1, 8), + 'newName' + ); + + assert.deepStrictEqual(result, { + changes: { + [getUri('rename-ignore-generated.svelte')]: [ + { + newText: 'newName', + range: { + end: { + character: 9, + line: 1 + }, + start: { + character: 8, + line: 1 + } + } + }, + { + newText: 'newName', + range: { + end: { + character: 6, + line: 5 + }, + start: { + character: 5, + line: 5 + } + } + }, + { + newText: 'newName', + range: { + end: { + character: 21, + line: 7 + }, + start: { + character: 20, + line: 7 + } + } + } + ] + } + }); + }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-imported.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-imported.svelte new file mode 100644 index 000000000..8da30703a --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-imported.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-shadowed.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-shadowed.svelte new file mode 100644 index 000000000..3e8000d1a --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow-shadowed.svelte @@ -0,0 +1,46 @@ + + +{#if typeof a === 'string'} + {a === true} + {assignA = a} + {#each [true] as a} + {a === true} + {assignA = a} + {/each} + {#if b} + {#await aPromise} + {b.a} + {:then b} + {b.a} + {/await} + {b.a} + {:else} + {b === true} + + {b.a} + {#if typeof b === 'boolean'} + {a === b} + {:else} + {a === b} + {/if} + + {/if} +{:else} + {#if typeof $store === 'string'} + {#each [] as a} + {$store === a} + {/each} + {:else} + {$store === a} + {/if} +{/if} \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow.svelte new file mode 100644 index 000000000..d4c19b844 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-if-control-flow.svelte @@ -0,0 +1,54 @@ + + +{#if typeof a === 'string'} + {a === true} + {assignA = a} + {#each [] as foo} + {a === true} + {assignA = a} + {foo} + {:else} + {a === true} + {assignA = a} + {/each} + {#if b} + {#await aPromise} + {b.a} + {:then x} + {b.a === x} + {/await} + {b.a} + {:else} + {b === true} + + {b.a === foo} + {#if typeof foo === 'boolean'} + {foo === a} + {:else} + {foo === a} + {/if} + + {/if} +{:else} + {#if typeof $store === 'string'} + {#each [] as foo} + {$store === a} + {foo} + {/each} + {:else} + {$store === a} + {/if} +{/if} + +{a === true} +{assignA = a} diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-ignore-generated.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-ignore-generated.svelte new file mode 100644 index 000000000..1441cf0f8 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/diagnostics-ignore-generated.svelte @@ -0,0 +1,12 @@ + + +{#if typeof a === 'string'} + {a === true} + {#each [true] as a} + {a === true} + {/each} + {#if b} + {b} + {/if} +{/if} diff --git a/packages/language-server/test/plugins/typescript/testfiles/find-references-ignore-generated.svelte b/packages/language-server/test/plugins/typescript/testfiles/find-references-ignore-generated.svelte new file mode 100644 index 000000000..693e7a15b --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/find-references-ignore-generated.svelte @@ -0,0 +1,12 @@ + + +{#if a} + {#await promise} + {#if typeof a === 'string'} + {promise} + {/if} + {/await} +{/if} diff --git a/packages/language-server/test/plugins/typescript/testfiles/rename/rename-ignore-generated.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-ignore-generated.svelte new file mode 100644 index 000000000..693e7a15b --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/rename/rename-ignore-generated.svelte @@ -0,0 +1,12 @@ + + +{#if a} + {#await promise} + {#if typeof a === 'string'} + {promise} + {/if} + {/await} +{/if} diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts index b5eb8ad87..81eb06f62 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts @@ -2,26 +2,29 @@ import { Node } from 'estree-walker'; import MagicString from 'magic-string'; import svelte from 'svelte/compiler'; import { parseHtmlx } from '../utils/htmlxparser'; +import { getSlotName } from '../utils/svelteAst'; import { handleActionDirective } from './nodes/action-directive'; import { handleAnimateDirective } from './nodes/animation-directive'; import { handleAttribute } from './nodes/attribute'; -import { handleAwait } from './nodes/await'; -import { handleKey } from './nodes/key'; +import { handleAwait, handleAwaitCatch, handleAwaitPending, handleAwaitThen } from './nodes/await'; import { handleBinding } from './nodes/binding'; import { handleClassDirective } from './nodes/class-directive'; import { handleComment } from './nodes/comment'; import { handleComponent } from './nodes/component'; -import { handleSlot } from './nodes/slot'; import { handleDebug } from './nodes/debug'; import { handleEach } from './nodes/each'; import { handleElement } from './nodes/element'; import { handleEventHandler } from './nodes/event-handler'; import { handleElse, handleIf } from './nodes/if-else'; +import { IfScope } from './nodes/if-scope'; +import { handleKey } from './nodes/key'; import { handleRawHtml } from './nodes/raw-html'; +import { handleSlot } from './nodes/slot'; import { handleSvelteTag } from './nodes/svelte-tag'; -import { handleTransitionDirective } from './nodes/transition-directive'; +import { TemplateScopeManager } from './nodes/template-scope'; import { handleText } from './nodes/text'; -import { getSlotName } from '../utils/svelteAst'; +import { handleTransitionDirective } from './nodes/transition-directive'; +import { usesLet } from './utils/node-utils'; type Walker = (node: Node, parent: Node, prop: string, index: number) => void; @@ -48,21 +51,43 @@ export function convertHtmlxToJsx( str.prepend('<>'); str.append(''); + const templateScopeManager = new TemplateScopeManager(); + + let ifScope = new IfScope(templateScopeManager); + (svelte as any).walk(ast, { enter: (node: Node, parent: Node, prop: string, index: number) => { try { switch (node.type) { case 'IfBlock': - handleIf(htmlx, str, node); + handleIf(htmlx, str, node, ifScope); + if (!node.elseif) { + ifScope = ifScope.getChild(); + } break; case 'EachBlock': - handleEach(htmlx, str, node); + templateScopeManager.eachEnter(node); + handleEach(htmlx, str, node, ifScope); break; case 'ElseBlock': - handleElse(htmlx, str, node, parent); + templateScopeManager.elseEnter(parent); + handleElse(htmlx, str, node, parent, ifScope); break; case 'AwaitBlock': - handleAwait(htmlx, str, node); + templateScopeManager.awaitEnter(node); + handleAwait(htmlx, str, node, ifScope); + break; + case 'PendingBlock': + templateScopeManager.awaitPendingEnter(node, parent); + handleAwaitPending(parent, htmlx, str, ifScope); + break; + case 'ThenBlock': + templateScopeManager.awaitThenEnter(node, parent); + handleAwaitThen(parent, htmlx, str, ifScope); + break; + case 'CatchBlock': + templateScopeManager.awaitCatchEnter(node, parent); + handleAwaitCatch(parent, htmlx, str, ifScope); break; case 'KeyBlock': handleKey(htmlx, str, node); @@ -74,10 +99,26 @@ export function convertHtmlxToJsx( handleDebug(htmlx, str, node); break; case 'InlineComponent': - handleComponent(htmlx, str, node, parent); + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + handleComponent( + htmlx, + str, + node, + parent, + ifScope, + templateScopeManager.value + ); break; case 'Element': - handleElement(htmlx, str, node, parent); + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + handleElement( + htmlx, + str, + node, + parent, + ifScope, + templateScopeManager.value + ); break; case 'Comment': handleComment(str, node); @@ -117,7 +158,18 @@ export function convertHtmlxToJsx( break; case 'SlotTemplate': handleSvelteTag(htmlx, str, node); - handleSlot(htmlx, str, node, parent, getSlotName(node) || 'default'); + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + if (usesLet(node)) { + handleSlot( + htmlx, + str, + node, + parent, + getSlotName(node) || 'default', + ifScope, + templateScopeManager.value + ); + } break; case 'Text': handleText(str, node); @@ -134,6 +186,22 @@ export function convertHtmlxToJsx( leave: (node: Node, parent: Node, prop: string, index: number) => { try { + switch (node.type) { + case 'IfBlock': + ifScope = ifScope.getParent(); + break; + case 'EachBlock': + templateScopeManager.eachLeave(node); + break; + case 'AwaitBlock': + templateScopeManager.awaitLeave(); + break; + case 'InlineComponent': + case 'Element': + case 'SlotTemplate': + templateScopeManager.componentOrSlotTemplateOrElementLeave(node); + break; + } if (onLeave) { onLeave(node, parent, prop, index); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts index ec4cb784f..67bcbd7c1 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts @@ -1,68 +1,110 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; +import { IfScope } from './if-scope'; /** * Transform {#await ...} into something JSX understands */ -export function handleAwait(htmlx: string, str: MagicString, awaitBlock: Node): void { +export function handleAwait( + htmlx: string, + str: MagicString, + awaitBlock: Node, + ifScope: IfScope +): void { // {#await somePromise then value} -> // {() => {let _$$p = (somePromise); - str.overwrite(awaitBlock.start, awaitBlock.expression.start, '{() => {let _$$p = ('); + const constRedeclares = ifScope.getConstsToRedeclare(); + str.overwrite( + awaitBlock.start, + awaitBlock.expression.start, + `{() => {${constRedeclares}let _$$p = (` + ); + + // {/await} -> + // <>})} + const awaitEndStart = htmlx.lastIndexOf('{', awaitBlock.end - 1); + str.overwrite(awaitEndStart, awaitBlock.end, '})}}'); +} + +export function handleAwaitPending( + awaitBlock: Node, + htmlx: string, + str: MagicString, + ifScope: IfScope +): void { + if (awaitBlock.pending.skip) { + return; + } + + // {await aPromise} ... -> aPromise); (possibleIfCondition &&)<> ... + const pendingStart = htmlx.indexOf('}', awaitBlock.expression.end); + const pendingEnd = !awaitBlock.then.skip + ? awaitBlock.then.start + : !awaitBlock.catch.skip + ? awaitBlock.catch.start + : htmlx.lastIndexOf('{', awaitBlock.end); + str.overwrite(awaitBlock.expression.end, pendingStart + 1, ');'); + str.appendRight(pendingStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); + str.appendLeft(pendingEnd, '; '); + + if (!awaitBlock.then.skip) { + return; + } + // no need to prepend ifcondition here as we know the then block is empty + str.appendLeft(pendingEnd, '__sveltets_awaitThen(_$$p, () => {<>'); +} + +export function handleAwaitThen( + awaitBlock: Node, + htmlx: string, + str: MagicString, + ifScope: IfScope +): void { + if (awaitBlock.then.skip) { + return; + } // then value } | {:then value} | {await ..} .. {/await} -> - // __sveltets_awaitThen(_$$p, (value) => {<> + // __sveltets_awaitThen(_$$p, (value) => {(possibleIfCondition && )<> let thenStart: number; let thenEnd: number; - if (!awaitBlock.then.skip) { - // then value } | {:then value} - if (!awaitBlock.pending.skip) { - // {await ...} ... {:then ...} - // thenBlock includes the {:then} - thenStart = awaitBlock.then.start; - if (awaitBlock.value) { - thenEnd = htmlx.indexOf('}', awaitBlock.value.end) + 1; - } else { - thenEnd = htmlx.indexOf('}', awaitBlock.then.start) + 1; - } - str.prependLeft(thenStart, '; '); - // add the start tag too - const awaitEnd = htmlx.indexOf('}', awaitBlock.expression.end); - - // somePromise} -> somePromise); - str.overwrite(awaitBlock.expression.end, awaitEnd + 1, ');'); - str.appendRight(awaitEnd + 1, ' <>'); + // then value } | {:then value} + if (!awaitBlock.pending.skip) { + // {await ...} ... {:then ...} + // thenBlock includes the {:then} + thenStart = awaitBlock.then.start; + if (awaitBlock.value) { + thenEnd = htmlx.indexOf('}', awaitBlock.value.end) + 1; } else { - // {await ... then ...} - thenStart = htmlx.indexOf('then', awaitBlock.expression.end); - thenEnd = htmlx.lastIndexOf('}', awaitBlock.then.start) + 1; - // somePromise then -> somePromise); then - str.overwrite(awaitBlock.expression.end, thenStart, '); '); + thenEnd = htmlx.indexOf('}', awaitBlock.then.start) + 1; } } else { - // {await ..} ... ({:catch ..}) {/await} -> no then block, no value, but always a pending block - thenEnd = awaitBlock.catch.skip - ? htmlx.lastIndexOf('{', awaitBlock.end) - : awaitBlock.catch.start; - thenStart = Math.min(awaitBlock.pending.end + 1, thenEnd); - - const awaitEnd = htmlx.indexOf('}', awaitBlock.expression.end); - str.overwrite(awaitBlock.expression.end, awaitEnd + 1, ');'); - str.appendRight(awaitEnd + 1, ' <>'); - str.appendLeft(thenEnd, '; '); + // {await ... then ...} + thenStart = htmlx.indexOf('then', awaitBlock.expression.end); + thenEnd = htmlx.lastIndexOf('}', awaitBlock.then.start) + 1; + // somePromise then -> somePromise); then + str.overwrite(awaitBlock.expression.end, thenStart, '); '); } if (awaitBlock.value) { str.overwrite(thenStart, awaitBlock.value.start, '__sveltets_awaitThen(_$$p, ('); - str.overwrite(awaitBlock.value.end, thenEnd, ') => {<>'); + str.overwrite(awaitBlock.value.end, thenEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); } else { - const awaitThenFn = '__sveltets_awaitThen(_$$p, () => {<>'; + const awaitThenFn = `__sveltets_awaitThen(_$$p, () => {${ifScope.addPossibleIfCondition()}<>`; // eslint-disable-line if (thenStart === thenEnd) { str.appendLeft(thenStart, awaitThenFn); } else { str.overwrite(thenStart, thenEnd, awaitThenFn); } } +} +export function handleAwaitCatch( + awaitBlock: Node, + htmlx: string, + str: MagicString, + ifScope: IfScope +): void { //{:catch error} -> //}, (error) => {<> if (!awaitBlock.catch.skip) { @@ -74,10 +116,6 @@ export function handleAwait(htmlx: string, str: MagicString, awaitBlock: Node): const errorEnd = awaitBlock.error ? awaitBlock.error.end : errorStart; const catchEnd = htmlx.indexOf('}', errorEnd) + 1; str.overwrite(catchStart, errorStart, '}, ('); - str.overwrite(errorEnd, catchEnd, ') => {<>'); + str.overwrite(errorEnd, catchEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); } - // {/await} -> - // <>})} - const awaitEndStart = htmlx.lastIndexOf('{', awaitBlock.end - 1); - str.overwrite(awaitEndStart, awaitBlock.end, '})}}'); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts index cfb611ac8..b98c53c0b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts @@ -2,11 +2,20 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import { getSlotName } from '../../utils/svelteAst'; import { handleSlot } from './slot'; +import { IfScope } from './if-scope'; +import { TemplateScope } from '../nodes/template-scope'; /** * Handle `` and slot-specific transformations. */ -export function handleComponent(htmlx: string, str: MagicString, el: Node, parent: Node): void { +export function handleComponent( + htmlx: string, + str: MagicString, + el: Node, + parent: Node, + ifScope: IfScope, + templateScope: TemplateScope +): void { //we need to remove : if it is a svelte component if (el.name.startsWith('svelte:')) { const colon = htmlx.indexOf(':', el.start); @@ -21,5 +30,13 @@ export function handleComponent(htmlx: string, str: MagicString, el: Node, paren // Handle possible slot const slotName = getSlotName(el) || 'default'; - handleSlot(htmlx, str, el, slotName === 'default' ? el : parent, slotName); + handleSlot( + htmlx, + str, + el, + slotName === 'default' ? el : parent, + slotName, + ifScope, + templateScope + ); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts index 6b8ef3040..174beca87 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts @@ -1,13 +1,21 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; +import { IfScope } from './if-scope'; /** * Transform each block into something JSX can understand. */ -export function handleEach(htmlx: string, str: MagicString, eachBlock: Node): void { +export function handleEach( + htmlx: string, + str: MagicString, + eachBlock: Node, + ifScope: IfScope +): void { // {#each items as item,i (key)} -> - // {__sveltets_each(items, (item,i) => (key) && <> - str.overwrite(eachBlock.start, eachBlock.expression.start, '{__sveltets_each('); + // {__sveltets_each(items, (item,i) => (key) && (possible if expression &&) <> + const constRedeclares = ifScope.getConstsToRedeclare(); + const prefix = constRedeclares ? `{() => {${constRedeclares}() => ` : ''; + str.overwrite(eachBlock.start, eachBlock.expression.start, `${prefix}{__sveltets_each(`); str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', ('); // {#each true, items as item} @@ -24,19 +32,21 @@ export function handleEach(htmlx: string, str: MagicString, eachBlock: Node): vo str.prependLeft(contextEnd, ') =>'); if (eachBlock.key) { const endEachStart = htmlx.indexOf('}', eachBlock.key.end); - str.overwrite(endEachStart, endEachStart + 1, ' && <>'); + str.overwrite(endEachStart, endEachStart + 1, ` && ${ifScope.addPossibleIfCondition()}<>`); } else { const endEachStart = htmlx.indexOf('}', contextEnd); - str.overwrite(endEachStart, endEachStart + 1, ' <>'); + str.overwrite(endEachStart, endEachStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); } + const endEach = htmlx.lastIndexOf('{', eachBlock.end - 1); + const suffix = constRedeclares ? ')}}}' : ')}'; // {/each} -> )} or {:else} -> )} if (eachBlock.else) { const elseEnd = htmlx.lastIndexOf('}', eachBlock.else.start); const elseStart = htmlx.lastIndexOf('{', elseEnd); - str.overwrite(elseStart, elseEnd + 1, ')}'); + str.overwrite(elseStart, elseEnd + 1, suffix); str.remove(endEach, eachBlock.end); } else { - str.overwrite(endEach, eachBlock.end, ')}'); + str.overwrite(endEach, eachBlock.end, suffix); } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts index 79094ad0c..120086b41 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts @@ -2,14 +2,23 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import { getSlotName } from '../../utils/svelteAst'; import { handleSlot } from './slot'; +import { IfScope } from './if-scope'; +import { TemplateScope } from '../nodes/template-scope'; /** * Special treatment for self-closing / void tags to make them conform to JSX. */ -export function handleElement(htmlx: string, str: MagicString, node: Node, parent: Node): void { +export function handleElement( + htmlx: string, + str: MagicString, + node: Node, + parent: Node, + ifScope: IfScope, + templateScope: TemplateScope +): void { const slotName = getSlotName(node); if (slotName) { - handleSlot(htmlx, str, node, parent, slotName); + handleSlot(htmlx, str, node, parent, slotName, ifScope, templateScope); } //we just have to self close void tags since jsx always wants the /> diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts index 59021a397..f59043285 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts @@ -1,10 +1,11 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; +import { IfScope } from './if-scope'; /** * {# if ...}...{/if} ---> {() => {if(...){<>...}}} */ -export function handleIf(htmlx: string, str: MagicString, ifBlock: Node): void { +export function handleIf(htmlx: string, str: MagicString, ifBlock: Node, ifScope: IfScope): void { const endIf = htmlx.lastIndexOf('{', ifBlock.end - 1); if (ifBlock.elseif) { @@ -14,6 +15,8 @@ export function handleIf(htmlx: string, str: MagicString, ifBlock: Node): void { str.overwrite(elseIfStart, ifBlock.expression.start, ' : (', { contentOnly: true }); str.overwrite(ifBlock.expression.end, elseIfConditionEnd, ') ? <>'); + ifScope.addElseIf(ifBlock.expression, str); + if (!ifBlock.else) { str.appendLeft(endIf, ' : <>'); } @@ -25,6 +28,8 @@ export function handleIf(htmlx: string, str: MagicString, ifBlock: Node): void { const end = htmlx.indexOf('}', ifBlock.expression.end); str.overwrite(ifBlock.expression.end, end + 1, ') ? <>', { contentOnly: true }); + ifScope.addNestedIf(ifBlock.expression, str); + if (ifBlock.else) { // {/if} -> } str.overwrite(endIf, ifBlock.end, ' }', { contentOnly: true }); @@ -37,7 +42,13 @@ export function handleIf(htmlx: string, str: MagicString, ifBlock: Node): void { /** * {:else} ---> : <> */ -export function handleElse(htmlx: string, str: MagicString, elseBlock: Node, parent: Node): void { +export function handleElse( + htmlx: string, + str: MagicString, + elseBlock: Node, + parent: Node, + ifScope: IfScope +): void { if ( parent.type !== 'IfBlock' || (elseBlock.children[0]?.type === 'IfBlock' && elseBlock.children[0]?.elseif) @@ -48,4 +59,6 @@ export function handleElse(htmlx: string, str: MagicString, elseBlock: Node, par const elseword = htmlx.lastIndexOf(':else', elseEnd); const elseStart = htmlx.lastIndexOf('{', elseword); str.overwrite(elseStart, elseEnd + 1, ' : <>'); + + ifScope.addElse(); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts new file mode 100644 index 000000000..9fdd39aa5 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts @@ -0,0 +1,403 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import { getIdentifiersInIfExpression } from '../utils/node-utils'; +import { TemplateScope } from '../nodes/template-scope'; +import { surroundWithIgnoreComments } from '../../utils/ignore'; + +enum IfType { + If, + ElseIf, + Else +} + +/** + * Contains the raw text of the condition and a map of identifiers + * used within that condition. + */ +interface ConditionInfo { + identifiers: Map>; + text: string; +} + +/** + * See `Condition` for an explanation of the structure. + */ +interface IfCondition { + type: IfType.If; + condition: ConditionInfo; +} + +/** + * See `Condition` for an explanation of the structure. + */ +interface ElseIfCondition { + type: IfType.ElseIf; + condition: ConditionInfo; + parent: IfCondition | ElseIfCondition; +} + +/** + * See `Condition` for an explanation of the structure. + */ +interface ElseCondition { + type: IfType.Else; + parent: IfCondition | ElseIfCondition; +} + +/** + * A condition is a nested structure which starts with IfCondition + * and ends with ElseIfCondition or ElseCondition. + * ElseIfCondition/ElseCondition have parents which mark the previous conditions. + * This means that the resulting structure is reversed (starts with the last condition). + * Example: + * + * ``` + * if (foo) { + * } else if (bar) { + * } else {} + * ``` + * + * is translated to + * + * ``` + * { + * type: 2, // Else + * parent: { + * type: 1, // ElseIf + * condition: { + * identifiers: [[bar, [{start: 0, end: 2}]]], + * text: 'bar' + * }, + * parent: { + * type: 0, // If + * condition: { + * identifiers: [[foo, [{start: 0, end: 2}]]], + * text: 'foo' + * } + * } + * ``` + */ +type Condition = IfCondition | ElseIfCondition | ElseCondition; + +/** + * Creates a new condition whos parent is the current condition + * and the leaf is the passed in condition info. + * See `Condition` for an explanation of the structure. + */ +function addElseIfCondition( + existingCondition: IfCondition | ElseIfCondition, + newCondition: ConditionInfo +): ElseIfCondition { + return { + parent: existingCondition, + condition: newCondition, + type: IfType.ElseIf + }; +} + +/** + * Creates a new condition whos parent is the current condition + * and the leaf is the else condition, where children can follow. + * See `Condition` for an explanation of the structure. + */ +function addElseCondition(existingCondition: IfCondition | ElseIfCondition): ElseCondition { + return { + parent: existingCondition, + type: IfType.Else + }; +} + +const REPLACEMENT_PREFIX = '\u03A9'; + +/** + * Returns the full currently known condition. Identifiers in the condition + * get replaced if they were redeclared. + */ +function getFullCondition( + condition: Condition, + replacedNames: string[], + replacementPrefix: string +): string { + switch (condition.type) { + case IfType.If: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + case IfType.ElseIf: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + case IfType.Else: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + } +} + +function _getFullCondition( + condition: Condition, + negate: boolean, + replacedNames: string[], + replacementPrefix: string +): string { + switch (condition.type) { + case IfType.If: + return negate + ? `!(${getConditionString(condition.condition, replacedNames, replacementPrefix)})` + : `(${getConditionString(condition.condition, replacedNames, replacementPrefix)})`; + case IfType.ElseIf: + return `${_getFullCondition( + condition.parent, + true, + replacedNames, + replacementPrefix + )} && ${negate ? '!' : ''}(${getConditionString( + condition.condition, + replacedNames, + replacementPrefix + )})`; + case IfType.Else: + return `${_getFullCondition(condition.parent, true, replacedNames, replacementPrefix)}`; + } +} + +/** + * Alter a condition text such that identifiers which needs replacement + * are replaced accordingly. + */ +function getConditionString( + condition: ConditionInfo, + replacedNames: string[], + replacementPrefix: string +): string { + const replacements: Array<{ name: string; start: number; end: number }> = []; + for (const name of replacedNames) { + const occurences = condition.identifiers.get(name); + if (occurences) { + for (const occurence of occurences) { + replacements.push({ ...occurence, name }); + } + } + } + + if (!replacements.length) { + return condition.text; + } + + replacements.sort((r1, r2) => r1.start - r2.start); + return ( + condition.text.substring(0, replacements[0].start) + + replacements + .map( + (replacement, idx) => + replacementPrefix + + replacement.name + + condition.text.substring(replacement.end, replacements[idx + 1]?.start) + ) + .join('') + ); +} + +/** + * Returns a set of all identifiers that were used in this condition + */ +function collectReferencedIdentifiers(condition: Condition | undefined): Set { + const identifiers = new Set(); + let current = condition; + while (current) { + if (current.type === IfType.ElseIf || current.type === IfType.If) { + for (const identifier of current.condition.identifiers.keys()) { + identifiers.add(identifier); + } + } + current = + current.type === IfType.ElseIf || current.type === IfType.Else + ? current.parent + : undefined; + } + return identifiers; +} + +/** + * A scope contains a if-condition including else(if) branches. + * The branches are added over time and the whole known condition is updated accordingly. + * + * This class is then mainly used to reprint if-conditions. This is necessary when + * a lambda-function is declared within the jsx-template because that function loses + * the control flow information. The reprint should be prepended to the jsx-content + * of the lambda function. + * + * Example: + * `{check ? {() => {

hi

}} : ''}` + * becomes + * `{check ? {() => {check &&

hi

}} : ''}` + * + * Most of the logic in here deals with the possibility of shadowed variables. + * Example: + * `{check ? {(check) => {

{check}

}} : ''}` + * becomes + * `{check ? {const Ωcheck = check;(check) => {Ωcheck &&

{check}

}} : ''}` + * + */ +export class IfScope { + private child?: IfScope; + private ownScope = this.scope.value; + private replacementPrefix = REPLACEMENT_PREFIX.repeat(this.computeDepth()); + + constructor( + private scope: { value: TemplateScope }, + private current?: Condition, + private parent?: IfScope + ) {} + + /** + * Returns the full currently known condition, prepended with the conditions + * of its parents. Identifiers in the condition get replaced if they were redeclared. + */ + getFullCondition(): string { + if (!this.current) { + return ''; + } + + const parentCondition = this.parent?.getFullCondition(); + const condition = `(${getFullCondition( + this.current, + this.getNamesThatNeedReplacement(), + this.replacementPrefix + )})`; + return parentCondition ? `(${parentCondition}) && ${condition}` : condition; + } + + /** + * Convenience method which invokes `getFullCondition` and adds a `&&` at the end + * for easy chaining. + */ + addPossibleIfCondition(): string { + const condition = this.getFullCondition(); + return condition ? surroundWithIgnoreComments(`${condition} && `) : ''; + } + + /** + * Adds a new child IfScope. + */ + addNestedIf(expression: Node, str: MagicString): void { + const condition = this.getConditionInfo(str, expression); + const ifScope = new IfScope(this.scope, { condition, type: IfType.If }, this); + this.child = ifScope; + } + + /** + * Adds a `else if` branch to the scope and enhances the condition accordingly. + */ + addElseIf(expression: Node, str: MagicString): void { + const condition = this.getConditionInfo(str, expression); + this.current = addElseIfCondition(this.current as IfCondition | ElseIfCondition, condition); + } + + /** + * Adds a `else` branch to the scope and enhances the condition accordingly. + */ + addElse(): void { + this.current = addElseCondition(this.current as IfCondition | ElseIfCondition); + } + + getChild(): IfScope { + return this.child || this; + } + + getParent(): IfScope { + return this.parent || this; + } + + /** + * Returns a set of all identifiers that were used in this IfScope and its parent scopes. + */ + collectReferencedIdentifiers(): Set { + const current = collectReferencedIdentifiers(this.current); + const parent = this.parent?.collectReferencedIdentifiers(); + if (parent) { + for (const identifier of parent) { + current.add(identifier); + } + } + return current; + } + + /** + * Should be invoked when a new template scope which resets control flow (await, each, slot) is created. + * The returned string contains a list of `const` declarations which redeclares the identifiers + * in the conditions which would be overwritten by the scope + * (because they declare a variable with the same name, therefore shadowing the outer variable). + */ + getConstsToRedeclare(): string { + const replacements = this.getNamesToRedeclare() + .map((identifier) => `${this.replacementPrefix + identifier}=${identifier}`) + .join(','); + return replacements ? surroundWithIgnoreComments(`const ${replacements};`) : ''; + } + + /** + * Returns true if given identifier is referenced in this IfScope or a parent scope. + */ + referencesIdentifier(name: string): boolean { + const current = collectReferencedIdentifiers(this.current); + if (current.has(name)) { + return true; + } + if (!this.parent || this.ownScope.inits.has(name)) { + return false; + } + return this.parent.referencesIdentifier(name); + } + + private getConditionInfo(str: MagicString, expression: Node): ConditionInfo { + const identifiers = getIdentifiersInIfExpression(expression); + const text = str.original.substring(expression.start, expression.end); + return { identifiers, text }; + } + + /** + * Contains a list of identifiers which would be overwritten by the child template scope. + */ + private getNamesToRedeclare() { + return [...this.scope.value.inits.keys()].filter((init) => { + let parent = this.scope.value.parent; + while (parent && parent !== this.ownScope) { + if (parent.inits.has(init)) { + return false; + } + parent = parent.parent; + } + return this.referencesIdentifier(init); + }); + } + + /** + * Return all identifiers that were redeclared and therefore need replacement. + */ + private getNamesThatNeedReplacement() { + const referencedIdentifiers = this.collectReferencedIdentifiers(); + return [...referencedIdentifiers].filter((identifier) => + this.someChildScopeHasRedeclaredVariable(identifier) + ); + } + + /** + * Returns true if given identifier name is redeclared in a child template scope + * and is therefore shadowed within that scope. + */ + private someChildScopeHasRedeclaredVariable(name: string) { + let scope = this.scope.value; + while (scope && scope !== this.ownScope) { + if (scope.inits.has(name)) { + return true; + } + scope = scope.parent; + } + return false; + } + + private computeDepth() { + let idx = 1; + let parent = this.ownScope.parent; + while (parent) { + idx++; + parent = parent.parent; + } + return idx; + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts index 5e6634119..b079a3576 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts @@ -2,13 +2,17 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import { beforeStart } from '../utils/node-utils'; import { getSingleSlotDef } from '../../svelte2tsx/nodes/slot'; +import { IfScope } from './if-scope'; +import { TemplateScope } from '../nodes/template-scope'; export function handleSlot( htmlx: string, str: MagicString, slotEl: Node, component: Node, - slotName: string + slotName: string, + ifScope: IfScope, + templateScope: TemplateScope ): void { //collect "let" definitions const slotElIsComponent = slotEl === component; @@ -39,6 +43,7 @@ export function handleSlot( } else { str.remove(attr.start, attr.start + 'let:'.length); } + templateScope.inits.add(attr.expression?.name || attr.name); hasMoved = true; if (attr.expression) { //overwrite the = as a : @@ -51,11 +56,17 @@ export function handleSlot( if (!hasMoved) { return; } - str.appendLeft(slotDefInsertionPoint, '{() => { let {'); - str.appendRight(slotDefInsertionPoint, `} = ${getSingleSlotDef(component, slotName)}` + ';<>'); + + const constRedeclares = ifScope.getConstsToRedeclare(); + const prefix = constRedeclares ? `() => {${constRedeclares}` : ''; + str.appendLeft(slotDefInsertionPoint, `{${prefix}() => { let {`); + str.appendRight( + slotDefInsertionPoint, + `} = ${getSingleSlotDef(component, slotName)}` + `;${ifScope.addPossibleIfCondition()}<>` + ); const closeSlotDefInsertionPoint = slotElIsComponent ? htmlx.lastIndexOf('<', slotEl.end - 1) : slotEl.end; - str.appendLeft(closeSlotDefInsertionPoint, '}}'); + str.appendLeft(closeSlotDefInsertionPoint, `}}${constRedeclares ? '}' : ''}`); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts new file mode 100644 index 000000000..fb108339f --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts @@ -0,0 +1,114 @@ +import { Node } from 'estree-walker'; +import { extract_identifiers } from 'periscopic'; +import { SvelteIdentifier } from '../../interfaces'; +import { isDestructuringPatterns, isIdentifier } from '../../utils/svelteAst'; +import { usesLet } from '../utils/node-utils'; + +export class TemplateScope { + inits = new Set(); + parent?: TemplateScope; + + constructor(parent?: TemplateScope) { + this.parent = parent; + } + + child() { + const child = new TemplateScope(this); + return child; + } +} + +export class TemplateScopeManager { + value = new TemplateScope(); + + eachEnter(node: Node) { + this.value = this.value.child(); + if (node.context) { + this.handleScope(node.context); + } + if (node.index) { + this.value.inits.add(node.index); + } + } + + eachLeave(node: Node) { + if (!node.else) { + this.value = this.value.parent; + } + } + + awaitEnter(node: Node) { + this.value = this.value.child(); + if (node.value) { + this.handleScope(node.value); + } + if (node.error) { + this.handleScope(node.error); + } + } + + awaitPendingEnter(node: Node, parent: Node) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, as pending can have no inits + this.value.inits.clear(); + } + + awaitThenEnter(node: Node, parent: Node) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, this time only taking the then + // scope into account. + this.value.inits.clear(); + if (parent.value) { + this.handleScope(parent.value); + } + } + + awaitCatchEnter(node: Node, parent: Node) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, this time only taking the error + // scope into account. + this.value.inits.clear(); + if (parent.error) { + this.handleScope(parent.error); + } + } + + awaitLeave() { + this.value = this.value.parent; + } + + elseEnter(parent: Node) { + if (parent.type === 'EachBlock') { + this.value = this.value.parent; + } + } + + componentOrSlotTemplateOrElementEnter(node: Node) { + if (usesLet(node)) { + this.value = this.value.child(); + } + } + + componentOrSlotTemplateOrElementLeave(node: Node) { + if (usesLet(node)) { + this.value = this.value.parent; + } + } + + private handleScope(identifierDef: Node) { + if (isIdentifier(identifierDef)) { + this.value.inits.add(identifierDef.name); + } + if (isDestructuringPatterns(identifierDef)) { + // the node object is returned as-it with no mutation + const identifiers = extract_identifiers(identifierDef) as SvelteIdentifier[]; + identifiers.forEach((id) => this.value.inits.add(id.name)); + } + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts index 4a1543f40..7174f53d0 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts @@ -1,4 +1,4 @@ -import { Node } from 'estree-walker'; +import { Node, walk } from 'estree-walker'; export function getTypeForComponent(node: Node): string { if (node.name === 'svelte:component' || node.name === 'svelte:self') { @@ -32,3 +32,38 @@ export function isShortHandAttribute(attr: Node): boolean { export function isQuote(str: string): boolean { return str === '"' || str === "'"; } + +export function getIdentifiersInIfExpression( + expression: Node +): Map> { + const offset = expression.start; + const identifiers = new Map>(); + walk(expression, { + enter: (node, parent) => { + switch (node.type) { + case 'Identifier': + // parent.property === node => node is "prop" in "obj.prop" + // parent.callee === node => node is "fun" in "fun(..)" + if (parent?.property !== node && parent?.callee !== node) { + add(node); + } + break; + } + } + }); + + function add(node: Node) { + let entry = identifiers.get(node.name); + if (!entry) { + entry = []; + } + entry.push({ start: node.start - offset, end: node.end - offset }); + identifiers.set(node.name, entry); + } + + return identifiers; +} + +export function usesLet(node: Node): boolean { + return node.attributes?.some((attr) => attr.type === 'Let'); +} diff --git a/packages/svelte2tsx/src/utils/ignore.ts b/packages/svelte2tsx/src/utils/ignore.ts new file mode 100644 index 000000000..81a370fc3 --- /dev/null +++ b/packages/svelte2tsx/src/utils/ignore.ts @@ -0,0 +1,10 @@ +export const IGNORE_START_COMMENT = '/*Ωignore_startΩ*/'; +export const IGNORE_END_COMMENT = '/*Ωignore_endΩ*/'; + +/** + * Surrounds given string with a start/end comment which marks it + * to be ignored by tooling. + */ +export function surroundWithIgnoreComments(str: string): string { + return IGNORE_START_COMMENT + str + IGNORE_END_COMMENT; +} diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/expected.jsx new file mode 100644 index 000000000..eadce747e --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/expected.jsx @@ -0,0 +1,80 @@ +<>{(hello) ? <> + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/let _$$p = (aPromise); __sveltets_awaitThen(_$$p, (hello) => {/*Ωignore_startΩ*/((Ωhello)) && /*Ωignore_endΩ*/<> + {hello} + }, () => {/*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> + {hello} + })}} + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/let _$$p = (aPromise); __sveltets_awaitThen(_$$p, (hi) => {/*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> + {hello} + }, (hello) => {/*Ωignore_startΩ*/((Ωhello)) && /*Ωignore_endΩ*/<> + {hello} + })}} + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/let _$$p = (hello); __sveltets_awaitThen(_$$p, (hello) => {/*Ωignore_startΩ*/((Ωhello)) && /*Ωignore_endΩ*/<> + {hello} + {(hello) ? <> + {() => {let _$$p = (aPromise); /*Ωignore_startΩ*/(((Ωhello))) && ((hello)) && /*Ωignore_endΩ*/<> + {hello} + ; __sveltets_awaitThen(_$$p, () => {<>})}} + {() => {/*Ωignore_startΩ*/const ΩΩhello=hello;/*Ωignore_endΩ*/let _$$p = (aPromise); /*Ωignore_startΩ*/(((Ωhello))) && ((hello)) && /*Ωignore_endΩ*/<> + {hello} + ; __sveltets_awaitThen(_$$p, () => {<>}, (hello) => {/*Ωignore_startΩ*/(((Ωhello))) && ((ΩΩhello)) && /*Ωignore_endΩ*/<> + {hello} + })}} + {() => {/*Ωignore_startΩ*/const ΩΩhello=hello;/*Ωignore_endΩ*/let _$$p = (x); __sveltets_awaitThen(_$$p, (hello) => {/*Ωignore_startΩ*/(((Ωhello))) && ((ΩΩhello)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + })}} + : <>} + })}} + {(hi && bye) ? <> + {() => {/*Ωignore_startΩ*/const Ωbye=bye,Ωhello=hello;/*Ωignore_endΩ*/let _$$p = (x); __sveltets_awaitThen(_$$p, (bye) => {/*Ωignore_startΩ*/(((hello))) && ((hi && Ωbye)) && /*Ωignore_endΩ*/<> + {bye} + }, (hello) => {/*Ωignore_startΩ*/(((Ωhello))) && ((hi && bye)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + })}} + : (cool) ? <> + {() => {/*Ωignore_startΩ*/const Ωcool=cool;/*Ωignore_endΩ*/let _$$p = (cool); /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> + loading + ; __sveltets_awaitThen(_$$p, (cool) => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (Ωcool)) && /*Ωignore_endΩ*/<> + {(cool) ? <> + {cool} + : <>} + }, (cool) => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (Ωcool)) && /*Ωignore_endΩ*/<> + z + })}} + {() => {/*Ωignore_startΩ*/const Ωcool=cool;/*Ωignore_endΩ*/let _$$p = (aPromise); /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> + loading + ; __sveltets_awaitThen(_$$p, (cool) => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (Ωcool)) && /*Ωignore_endΩ*/<> + {cool} + })}} + : <> + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/let _$$p = (x); __sveltets_awaitThen(_$$p, (hello) => {/*Ωignore_startΩ*/(((Ωhello))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + })}} + } + : <>} + +{() => {let _$$p = (cool); <> + {(cool) ? <> + {cool} + : (hello) ? <> + {hello} + : <> } +; __sveltets_awaitThen(_$$p, (cool) => {<> + {(cool) ? <> + {cool} + : (hello) ? <> + {hello} + : <> } +}, (cool) => {<> + {(cool) ? <> + {cool} + : (hello) ? <> + {hello} + : <> } +})}} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/input.svelte new file mode 100644 index 000000000..3b9dc7fdd --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block-shadowed/input.svelte @@ -0,0 +1,80 @@ +{#if hello} + {#await aPromise then hello} + {hello} + {:catch} + {hello} + {/await} + {#await aPromise then hi} + {hello} + {:catch hello} + {hello} + {/await} + {#await hello then hello} + {hello} + {#if hello} + {#await aPromise} + {hello} + {/await} + {#await aPromise} + {hello} + {:catch hello} + {hello} + {/await} + {#await x then hello} + {#if hello} + {hello} + {/if} + {/await} + {/if} + {/await} + {#if hi && bye} + {#await x then bye} + {bye} + {:catch hello} + {#if hello} + {hello} + {/if} + {/await} + {:else if cool} + {#await cool} + loading + {:then cool} + {#if cool} + {cool} + {/if} + {:catch cool} + z + {/await} + {#await aPromise} + loading + {:then cool} + {cool} + {/await} + {:else} + {#await x then hello} + {#if hello} + {hello} + {/if} + {/await} + {/if} +{/if} + +{#await cool} + {#if cool} + {cool} + {:else if hello} + {hello} + {/if} +{:then cool} + {#if cool} + {cool} + {:else if hello} + {hello} + {/if} +{:catch cool} + {#if cool} + {cool} + {:else if hello} + {hello} + {/if} +{/await} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/expected.jsx new file mode 100644 index 000000000..df23e0a45 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/expected.jsx @@ -0,0 +1,27 @@ +<>{(hello) ? <> + {() => {let _$$p = (x); __sveltets_awaitThen(_$$p, (y) => {/*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> + {y} + })}} + {() => {let _$$p = (aPromise); /*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> + {hello} + ; __sveltets_awaitThen(_$$p, () => {<>})}} + {(hi && bye) ? <> + {() => {let _$$p = (x); __sveltets_awaitThen(_$$p, (y) => {/*Ωignore_startΩ*/(((hello))) && ((hi && bye)) && /*Ωignore_endΩ*/<> + {y} + }, () => {/*Ωignore_startΩ*/(((hello))) && ((hi && bye)) && /*Ωignore_endΩ*/<> + z + })}} + : (cool) ? <> + {() => {let _$$p = (x); /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> + loading + ; __sveltets_awaitThen(_$$p, (y) => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> + {y} + }, () => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> + z + })}} + : <> + {() => {let _$$p = (x); __sveltets_awaitThen(_$$p, (y) => {/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<> + {y} + })}} + } + : <>} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/input.svelte new file mode 100644 index 000000000..dfc6c93b1 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-await-block/input.svelte @@ -0,0 +1,27 @@ +{#if hello} + {#await x then y} + {y} + {/await} + {#await aPromise} + {hello} + {/await} + {#if hi && bye} + {#await x then y} + {y} + {:catch} + z + {/await} + {:else if cool} + {#await x} + loading + {:then y} + {y} + {:catch} + z + {/await} + {:else} + {#await x then y} + {y} + {/await} + {/if} +{/if} diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/expected.jsx new file mode 100644 index 000000000..240661761 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/expected.jsx @@ -0,0 +1,51 @@ +<>{(hello) ? <> + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/() => {__sveltets_each(items, (hello,i) => (hello.id) && /*Ωignore_startΩ*/((Ωhello)) && /*Ωignore_endΩ*/<> +
{hello}{i}
+ {(hello) ? <> + {() => {/*Ωignore_startΩ*/const ΩΩhello=hello;/*Ωignore_endΩ*/() => {__sveltets_each(items, (hello) => /*Ωignore_startΩ*/(((Ωhello))) && ((ΩΩhello)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + )}}} + : <>} + )}}} + {(hello) ? <> + {hello} + : <>} + + {(hi && bye) ? <> + {() => {/*Ωignore_startΩ*/const Ωbye=bye;/*Ωignore_endΩ*/() => {__sveltets_each(items, (bye) => /*Ωignore_startΩ*/(((hello))) && ((hi && Ωbye)) && /*Ωignore_endΩ*/<> +
{bye}
+ )}}} + {(bye) ? <> + {bye} + : <>} + + : (cool) ? <> + {() => {/*Ωignore_startΩ*/const Ωcool=cool;/*Ωignore_endΩ*/() => {__sveltets_each(items, (item,cool) => /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (Ωcool)) && /*Ωignore_endΩ*/<> +
{item}{cool}
+ )}}} + : <> + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/() => {__sveltets_each(items, (hello) => /*Ωignore_startΩ*/(((Ωhello))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<> +
{hello}
+ )}}} + } + : <>} + +{__sveltets_each(items, (hello,i) => <> + {(hello && i && bye) ? <> + {hello} {i} {bye} + : (hello && i && bye) ? <> + {hello} {i} {bye} + : <> + {hello} {i} {bye} + } +)} + {(hello && i && bye) ? <> + {hello} {i} {bye} + : (hello && i && bye) ? <> + {hello} {i} {bye} + : <> + {hello} {i} {bye} + } + \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/input.svelte new file mode 100644 index 000000000..5391a4393 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block-shadowed/input.svelte @@ -0,0 +1,51 @@ +{#if hello} + {#each items as hello,i (hello.id)} +
{hello}{i}
+ {#if hello} + {#each items as hello} + {#if hello} + {hello} + {/if} + {/each} + {/if} + {:else} + {#if hello} + {hello} + {/if} + {/each} + {#if hi && bye} + {#each items as bye} +
{bye}
+ {:else} + {#if bye} + {bye} + {/if} + {/each} + {:else if cool} + {#each items as item,cool} +
{item}{cool}
+ {/each} + {:else} + {#each items as hello} +
{hello}
+ {/each} + {/if} +{/if} + +{#each items as hello,i} + {#if hello && i && bye} + {hello} {i} {bye} + {:else if hello && i && bye} + {hello} {i} {bye} + {:else} + {hello} {i} {bye} + {/if} +{:else} + {#if hello && i && bye} + {hello} {i} {bye} + {:else if hello && i && bye} + {hello} {i} {bye} + {:else} + {hello} {i} {bye} + {/if} +{/each} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/expected.jsx new file mode 100644 index 000000000..f9e7204fd --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/expected.jsx @@ -0,0 +1,20 @@ +<>{(hello) ? <> + {__sveltets_each(items, (item,i) => (item.id) && /*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> +
{item}{i}
+ )} + {(hi && bye) ? <> + {__sveltets_each(items, (item) => /*Ωignore_startΩ*/(((hello))) && ((hi && bye)) && /*Ωignore_endΩ*/<> +
{item}
+ )} +

hi

+ + : (cool) ? <> + {__sveltets_each(items, (item,i) => /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<> +
{item}{i}
+ )} + : <> + {__sveltets_each(items, (item) => /*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<> +
{item}
+ )} + } + : <>} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/input.svelte new file mode 100644 index 000000000..f30d8bc4d --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-each-block/input.svelte @@ -0,0 +1,20 @@ +{#if hello} + {#each items as item,i (item.id)} +
{item}{i}
+ {/each} + {#if hi && bye} + {#each items as item} +
{item}
+ {:else} +

hi

+ {/each} + {:else if cool} + {#each items as item,i} +
{item}{i}
+ {/each} + {:else} + {#each items as item} +
{item}
+ {/each} + {/if} +{/if} diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/expected.jsx new file mode 100644 index 000000000..65d1fd704 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/expected.jsx @@ -0,0 +1,77 @@ +<>{(hello && hello1) ? <> + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/((Ωhello && hello1)) && /*Ωignore_endΩ*/<> + {hello} + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/((Ωhello && hello1)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + }} + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['named1'];/*Ωignore_startΩ*/((Ωhello && hello1)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + }} + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['named2'];/*Ωignore_startΩ*/((Ωhello && hello1)) && /*Ωignore_endΩ*/<>

+ {(hello) ? <> + {hello} + : <>} +

}} + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['named3'];/*Ωignore_startΩ*/((Ωhello && hello1)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + }} + {(hello) ? <> + {() => {/*Ωignore_startΩ*/const ΩΩhello=hello;/*Ωignore_endΩ*/() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/(((Ωhello && hello1))) && ((ΩΩhello)) && /*Ωignore_endΩ*/<> + {(hello) ? <> + {hello} + : <>} + }}} + : <>} + }}}
+ {(hi && bye) ? <> + {() => {/*Ωignore_startΩ*/const Ωbye=bye;/*Ωignore_endΩ*/() => { let {foo:bye} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/(((hello && hello1))) && ((hi && Ωbye)) && /*Ωignore_endΩ*/<> + {bye} + }}} + : (cool) ? <> + + {() => {/*Ωignore_startΩ*/const Ωcool=cool,Ωhello=hello;/*Ωignore_endΩ*/() => { let {cool, hello} = __sveltets_instanceOf(Comp).$$slot_def['named'];/*Ωignore_startΩ*/(((Ωhello && hello1))) && (!(hi && bye) && (Ωcool)) && /*Ωignore_endΩ*/<>
+ {hello} +
}}} +
+ : <> + + {() => {/*Ωignore_startΩ*/const Ωhello=hello;/*Ωignore_endΩ*/() => { let {foo:hello, hello1:other} = __sveltets_instanceOf(Comp).$$slot_def['named'];/*Ωignore_startΩ*/(((Ωhello && hello1))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<>
+ {hello} +
}}} +
+ } + : <>} + +{() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['default'];<> + {(hello && bye) ? <> + {hello} {bye} + : (hello && bye) ? <> + {hello} {bye} + : <> + {hello} {bye} + } + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['named1'];<> + {(hello && bye) ? <> + {hello} {bye} + : (hello && bye) ? <> + {hello} {bye} + : <> + {hello} {bye} + } + }} + {() => { let {hello} = __sveltets_instanceOf(Comp).$$slot_def['named2'];<>

+ {(hello && bye) ? <> + {hello} {bye} + : (hello && bye) ? <> + {hello} {bye} + : <> + {hello} {bye} + } +

}} +}}
\ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/input.svelte new file mode 100644 index 000000000..6d3489916 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let-shadowed/input.svelte @@ -0,0 +1,77 @@ +{#if hello && hello1} + + {hello} + + {#if hello} + {hello} + {/if} + + + {#if hello} + {hello} + {/if} + +

+ {#if hello} + {hello} + {/if} +

+ + {#if hello} + {hello} + {/if} + + {#if hello} + + {#if hello} + {hello} + {/if} + + {/if} +
+ {#if hi && bye} + + {bye} + + {:else if cool} + +
+ {hello} +
+
+ {:else} + +
+ {hello} +
+
+ {/if} +{/if} + + + {#if hello && bye} + {hello} {bye} + {:else if hello && bye} + {hello} {bye} + {:else} + {hello} {bye} + {/if} + + {#if hello && bye} + {hello} {bye} + {:else if hello && bye} + {hello} {bye} + {:else} + {hello} {bye} + {/if} + +

+ {#if hello && bye} + {hello} {bye} + {:else if hello && bye} + {hello} {bye} + {:else} + {hello} {bye} + {/if} +

+
\ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/expected.jsx new file mode 100644 index 000000000..768079481 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/expected.jsx @@ -0,0 +1,22 @@ +<>{(hello) ? <> + {() => { let {foo} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/((hello)) && /*Ωignore_endΩ*/<> + {foo} + }} + {(hi && bye) ? <> + {() => { let {foo:bar} = __sveltets_instanceOf(Comp).$$slot_def['default'];/*Ωignore_startΩ*/(((hello))) && ((hi && bye)) && /*Ωignore_endΩ*/<> + {bar} + }} + : (cool) ? <> + + {() => { let {foo, foo1} = __sveltets_instanceOf(Comp).$$slot_def['named'];/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && (cool)) && /*Ωignore_endΩ*/<>
+ {foo} +
}} +
+ : <> + + {() => { let {foo:bar} = __sveltets_instanceOf(Comp).$$slot_def['named'];/*Ωignore_startΩ*/(((hello))) && (!(hi && bye) && !(cool)) && /*Ωignore_endΩ*/<>
+ {bar} +
}} +
+ } + : <>} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/input.svelte new file mode 100644 index 000000000..7d2188ed8 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/if-nested-slot-let/input.svelte @@ -0,0 +1,22 @@ +{#if hello} + + {foo} + + {#if hi && bye} + + {bar} + + {:else if cool} + +
+ {foo} +
+
+ {:else} + +
+ {bar} +
+
+ {/if} +{/if} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components-let-forward/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components-let-forward/expected.tsx index d3a14e4f1..5f6c5df29 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components-let-forward/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components-let-forward/expected.tsx @@ -1,7 +1,7 @@ /// <>;function render() { <>{(true) ? <> -{() => { let {prop} = __sveltets_instanceOf(__sveltets_componentType()).$$slot_def['default'];<> +{() => { let {prop} = __sveltets_instanceOf(__sveltets_componentType()).$$slot_def['default'];/*Ωignore_startΩ*/((true)) && /*Ωignore_endΩ*/<> }} : <>} @@ -11,4 +11,4 @@ return { props: {}, slots: {'default': {prop:__sveltets_instanceOf(__sveltets_componentType()).$$slot_def['default'].prop}}, getters: {}, events: {} }} export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) { -} +} \ No newline at end of file