From 7f6a340577808ec3c454b7335eb05b41b1834ad5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 19 Jun 2021 11:58:24 +0200 Subject: [PATCH 1/4] (feat) implement experimental $$Props feature This implements the experimantal feature of defining a type or interface named $$Props that will check against the actual props of a components. #442 https://github.com/sveltejs/rfcs/pull/38 --- .../features/DiagnosticsProvider.ts | 51 ++++-- .../typescript/features/RenameProvider.ts | 13 +- .../src/plugins/typescript/features/utils.ts | 8 + packages/language-server/src/utils.ts | 13 ++ .../features/DiagnosticsProvider.test.ts | 165 ++++++++++++++++++ .../diagnostics/$$props-invalid1.svelte | 7 + .../diagnostics/$$props-invalid2.svelte | 7 + .../diagnostics/$$props-invalid3.svelte | 7 + .../diagnostics/$$props-valid.svelte | 9 + .../diagnostics/using-$$props.svelte | 12 ++ .../src/svelte2tsx/nodes/ExportedNames.ts | 137 ++++++++++++--- .../processInstanceScriptContent.ts | 32 +++- .../svelte2tsx/src/svelte2tsx/svelteShims.ts | 1 + packages/svelte2tsx/svelte-shims.d.ts | 1 + .../samples/creates-dts/expected.tsx | 1 + .../samples/export-list/expected.tsx | 20 ++- .../samples/export-list/input.svelte | 18 +- .../samples/ts-$$Props-interface/expected.tsx | 37 ++++ .../samples/ts-$$Props-interface/input.svelte | 30 ++++ .../samples/ts-$$Props-type/expected.tsx | 37 ++++ .../samples/ts-$$Props-type/input.svelte | 30 ++++ .../samples/ts-$$generics-dts/expected.tsx | 1 + .../samples/ts-creates-dts/expected.tsx | 1 + .../samples/ts-export-list/expected.tsx | 24 +++ .../samples/ts-export-list/input.svelte | 17 ++ .../uses-accessors-attr-present/expected.tsx | 2 +- .../expected.tsx | 2 +- 27 files changed, 622 insertions(+), 61 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid1.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid2.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid3.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-valid.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-$$props.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/input.svelte diff --git a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts index a00e4852b..f0334143d 100644 --- a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts @@ -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) {} @@ -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) @@ -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 diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 7e1182d2b..0808183a4 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -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) {} @@ -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); } /** diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index b82bff13e..e26582fdf 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -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; + } +} diff --git a/packages/language-server/src/utils.ts b/packages/language-server/src/utils.ts index 8f704ebd2..dbe8b65f0 100644 --- a/packages/language-server/src/utils.ts +++ b/packages/language-server/src/utils.ts @@ -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. */ 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 11c220a41..48040ca7f 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -1582,4 +1582,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: [] + } + ]); + }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid1.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid1.svelte new file mode 100644 index 000000000..1e6461202 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid1.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid2.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid2.svelte new file mode 100644 index 000000000..f612d827e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid2.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid3.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid3.svelte new file mode 100644 index 000000000..d37066d42 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-invalid3.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-valid.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-valid.svelte new file mode 100644 index 000000000..141deecb7 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$props-valid.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-$$props.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-$$props.svelte new file mode 100644 index 000000000..74340e9ed --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-$$props.svelte @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index 005f2e7a3..48d7c31e5 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -1,27 +1,68 @@ import ts from 'typescript'; -import { getLastLeadingDoc } from '../utils/tsAst'; +import { getLastLeadingDoc, isInterfaceOrTypeDeclaration } from '../utils/tsAst'; export interface IExportedNames { has(name: string): boolean; } -export class ExportedNames - extends Map< - string, - { - type?: string; - identifierText?: string; - required?: boolean; - doc?: string; +export function is$$PropsDeclaration( + node: ts.Node +): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration { + return isInterfaceOrTypeDeclaration(node) && node.name.text === '$$Props'; +} + +interface ExportedName { + isLet: boolean; + type?: string; + identifierText?: string; + required?: boolean; + doc?: string; +} + +export class ExportedNames extends Map implements IExportedNames { + private uses$$Props = false; + private possibleExports = new Map(); + + setUses$$Props(): void { + this.uses$$Props = true; + } + + /** + * Marks a top level declaration as a possible export + * which could be exported through `export { .. }` later. + */ + addPossibleExport( + name: ts.BindingName, + isLet: boolean, + target: ts.BindingName = null, + type: ts.TypeNode = null, + required = false + ) { + if (!ts.isIdentifier(name)) { + return; } - > - implements IExportedNames -{ + + if (target && ts.isIdentifier(target)) { + this.possibleExports.set(name.text, { + isLet, + type: type?.getText(), + identifierText: (target as ts.Identifier).text, + required, + doc: this.getDoc(target) + }); + } else { + this.possibleExports.set(name.text, { + isLet + }); + } + } + /** * Adds export to map */ addExport( name: ts.BindingName, + isLet: boolean, target: ts.BindingName = null, type: ts.TypeNode = null, required = false @@ -33,15 +74,22 @@ export class ExportedNames throw Error('export target kind not supported ' + target); } + const existingDeclaration = this.possibleExports.get(name.text); if (target) { this.set(name.text, { - type: type?.getText(), + isLet: isLet || existingDeclaration?.isLet, + type: type?.getText() || existingDeclaration?.type, identifierText: (target as ts.Identifier).text, - required, - doc: this.getDoc(target) + required: required || existingDeclaration?.required, + doc: this.getDoc(target) || existingDeclaration?.doc }); } else { - this.set(name.text, {}); + this.set(name.text, { + isLet: isLet || existingDeclaration?.isLet, + type: existingDeclaration?.type, + required: existingDeclaration?.required, + doc: existingDeclaration?.doc + }); } } @@ -62,27 +110,62 @@ export class ExportedNames * * @param isTsFile Whether this is a TypeScript file or not. */ - createPropsStr(isTsFile: boolean) { + createPropsStr(isTsFile: boolean): string { const names = Array.from(this.entries()); + + if (this.uses$$Props) { + const lets = names.filter(([, { isLet }]) => isLet); + const others = names.filter(([, { isLet }]) => !isLet); + // We need to check both ways: + // - The check if exports are assignable to Parial<$$Props> is necessary to make sure + // no props are missing. Partial<$$Props> is needed because props with a default value + // count as optional, but semantically speaking it is still correctly implementing the interface + // - The check if $$Props is assignable to exports is necessary to make sure no extraneous props + // are defined and that no props are required that should be optional + // __sveltets_ensureRightProps needs to be declared in a way that doesn't affect the type result of props + return ( + '{...__sveltets_ensureRightProps<{' + + this.createReturnElementsType(lets).join(',') + + '}>(__sveltets_any("") as $$Props), ' + + '...__sveltets_ensureRightProps>({' + + this.createReturnElements(lets, false).join(',') + + '}), ...{} as unknown as $$Props, ...{' + + this.createReturnElements(others, false).join(', ') + + '} as {' + + this.createReturnElementsType(others).join(',') + + '}}' + ); + } + const dontAddTypeDef = !isTsFile || names.length === 0 || names.every(([_, value]) => !value.type && value.required); + const returnElements = this.createReturnElements(names, dontAddTypeDef); + if (dontAddTypeDef) { + // No exports or only `typeof` exports -> omit the `as {...}` completely. + // If not TS, omit the types to not have a "cannot use types in jsx" error. + return `{${returnElements.join(' , ')}}`; + } - const returnElements = names.map(([key, value]) => { + const returnElementsType = this.createReturnElementsType(names); + return `{${returnElements.join(' , ')}} as {${returnElementsType.join(', ')}}`; + } + + private createReturnElements( + names: [string, ExportedName][], + dontAddTypeDef: boolean + ): string[] { + return names.map(([key, value]) => { // Important to not use shorthand props for rename functionality return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${ value.identifierText || key }: ${key}`; }); + } - if (dontAddTypeDef) { - // No exports or only `typeof` exports -> omit the `as {...}` completely. - // If not TS, omit the types to not have a "cannot use types in jsx" error. - return `{${returnElements.join(' , ')}}`; - } - - const returnElementsType = names.map(([key, value]) => { + private createReturnElementsType(names: [string, ExportedName][]) { + return names.map(([key, value]) => { const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${ value.required ? '' : '?' }`; @@ -92,11 +175,9 @@ export class ExportedNames return `${identifier}: ${value.type}`; }); - - return `{${returnElements.join(' , ')}} as {${returnElementsType.join(', ')}}`; } - createOptionalPropsArray() { + createOptionalPropsArray(): string[] { return Array.from(this.entries()) .filter(([_, entry]) => !entry.required) .map(([name, entry]) => `'${entry.identifierText || name}'`); diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index 71dec7fe4..c2db71d7f 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -7,7 +7,7 @@ import { isFirstInAnExpressionStatement, isNotPropertyNameOfImport } from './utils/tsAst'; -import { ExportedNames } from './nodes/ExportedNames'; +import { ExportedNames, is$$PropsDeclaration } from './nodes/ExportedNames'; import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames'; import { ComponentEvents, is$$EventsDeclaration } from './nodes/ComponentEvents'; import { Scope } from './utils/Scope'; @@ -143,7 +143,7 @@ export function processInstanceScriptContent( // Can't export default here if (node.name) { - exportedNames.addExport(node.name); + exportedNames.addExport(node.name, false); } }; @@ -309,18 +309,22 @@ export function processInstanceScriptContent( } }; - const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => { + const handleExportedVariableDeclarationList = ( + list: ts.VariableDeclarationList, + add: typeof exportedNames.addExport + ) => { + const isLet = list.flags === ts.NodeFlags.Let; ts.forEachChild(list, (node) => { if (ts.isVariableDeclaration(node)) { if (ts.isIdentifier(node.name)) { - exportedNames.addExport(node.name, node.name, node.type, !node.initializer); + add(node.name, isLet, node.name, node.type, !node.initializer); } else if ( ts.isObjectBindingPattern(node.name) || ts.isArrayBindingPattern(node.name) ) { ts.forEachChild(node.name, (element) => { if (ts.isBindingElement(element)) { - exportedNames.addExport(element.name); + add(element.name, isLet); } }); } @@ -360,6 +364,9 @@ export function processInstanceScriptContent( if (is$$SlotsDeclaration(node)) { uses$$SlotsInterface = true; } + if (is$$PropsDeclaration(node)) { + exportedNames.setUses$$Props(); + } if (ts.isVariableStatement(node)) { const exportModifier = findExportKeyword(node); @@ -367,7 +374,10 @@ export function processInstanceScriptContent( const isLet = node.declarationList.flags === ts.NodeFlags.Let; const isConst = node.declarationList.flags === ts.NodeFlags.Const; - handleExportedVariableDeclarationList(node.declarationList); + handleExportedVariableDeclarationList( + node.declarationList, + exportedNames.addExport.bind(exportedNames) + ); if (isLet) { propTypeAssertToUserDefined(node.declarationList); } else if (isConst) { @@ -379,6 +389,12 @@ export function processInstanceScriptContent( } removeExport(exportModifier.getStart(), exportModifier.end); } + if (ts.isSourceFile(parent)) { + handleExportedVariableDeclarationList( + node.declarationList, + exportedNames.addPossibleExport.bind(exportedNames) + ); + } } if (ts.isFunctionDeclaration(node)) { @@ -407,9 +423,9 @@ export function processInstanceScriptContent( if (ts.isNamedExports(exportClause)) { for (const ne of exportClause.elements) { if (ne.propertyName) { - exportedNames.addExport(ne.propertyName, ne.name); + exportedNames.addExport(ne.propertyName, false, ne.name); } else { - exportedNames.addExport(ne.name); + exportedNames.addExport(ne.name, false); } } //we can remove entire statement diff --git a/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts b/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts index 84b724fae..467006f93 100644 --- a/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts +++ b/packages/svelte2tsx/src/svelte2tsx/svelteShims.ts @@ -112,6 +112,7 @@ declare function __sveltets_ensureTransition(transitionCall: SvelteTransitionRet declare function __sveltets_ensureFunction(expression: (e: Event & { detail?: any }) => unknown ): {}; declare function __sveltets_ensureType(type: AConstructorTypeOf, el: T): {}; declare function __sveltets_createEnsureSlot>>(): (k1: K1, k2: K2, val: Slots[K1][K2]) => Slots[K1][K2]; +declare function __sveltets_ensureRightProps(props: Props): {}; declare function __sveltets_cssProp(prop: Record): {}; declare function __sveltets_ctorOf(type: T): AConstructorTypeOf; declare function __sveltets_instanceOf(type: AConstructorTypeOf): T; diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts index d6b84da38..dfe80d151 100644 --- a/packages/svelte2tsx/svelte-shims.d.ts +++ b/packages/svelte2tsx/svelte-shims.d.ts @@ -113,6 +113,7 @@ declare function __sveltets_ensureTransition(transitionCall: SvelteTransitionRet declare function __sveltets_ensureFunction(expression: (e: Event & { detail?: any }) => unknown ): {}; declare function __sveltets_ensureType(type: AConstructorTypeOf, el: T): {}; declare function __sveltets_createEnsureSlot>>(): (k1: K1, k2: K2, val: Slots[K1][K2]) => Slots[K1][K2]; +declare function __sveltets_ensureRightProps(props: Props): {}; declare function __sveltets_cssProp(prop: Record): {}; declare function __sveltets_ctorOf(type: T): AConstructorTypeOf; declare function __sveltets_instanceOf(type: AConstructorTypeOf): T; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx index deef3d6b0..52bd008c0 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/creates-dts/expected.tsx @@ -110,6 +110,7 @@ declare function __sveltets_ensureTransition(transitionCall: SvelteTransitionRet declare function __sveltets_ensureFunction(expression: (e: Event & { detail?: any }) => unknown ): {}; declare function __sveltets_ensureType(type: AConstructorTypeOf, el: T): {}; declare function __sveltets_createEnsureSlot>>(): (k1: K1, k2: K2, val: Slots[K1][K2]) => Slots[K1][K2]; +declare function __sveltets_ensureRightProps(props: Props): {}; declare function __sveltets_cssProp(prop: Record): {}; declare function __sveltets_ctorOf(type: T): AConstructorTypeOf; declare function __sveltets_instanceOf(type: AConstructorTypeOf): T; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/export-list/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/export-list/expected.tsx index 9d0c2abc1..594846627 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/export-list/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/export-list/expected.tsx @@ -1,12 +1,24 @@ /// <>;function render() { - let name = "world" - let name2 = "world" + let name1 = "world" + let name2 + + let rename1 = ''; + let rename2; + + class Foo {} + function bar() {} + const baz = ''; + + class RenameFoo {} + function renamebar() {} + const renamebaz = ''; + ; () => (<>); -return { props: {name: name , name2: name2}, slots: {}, getters: {}, events: {} }} +return { props: {name1: name1 , name2: name2 , renamed1: rename1 , renamed2: rename2 , Foo: Foo , bar: bar , baz: baz , RenamedFoo: RenameFoo , renamedbar: renamebar , renamedbaz: renamebaz}, slots: {}, getters: {}, events: {} }} -export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['name','name2'], __sveltets_with_any_event(render()))) { +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['name1','renamed1','Foo','bar','baz','RenamedFoo','renamedbar','renamedbaz'], __sveltets_with_any_event(render()))) { } \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/export-list/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/export-list/input.svelte index 8d5929e12..036b2c529 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/export-list/input.svelte +++ b/packages/svelte2tsx/test/svelte2tsx/samples/export-list/input.svelte @@ -1,5 +1,17 @@ diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/expected.tsx new file mode 100644 index 000000000..e4b7bb756 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/expected.tsx @@ -0,0 +1,37 @@ +/// +<>;function render() { + + + type $$Props = { + exported1: string; + exported2?: string; + name1?: string; + name2: string; + renamed1?: string; + renamed2: string; + } + + let exported1: string; + let exported2: string = '';exported2 = __sveltets_any(exported2);; + + let name1: string = "world" + let name2: string; + + let rename1: string = ''; + let rename2: string; + + class Foo {} + function bar() {} + const baz: string = ''; + + class RenameFoo {} + function renamebar() {} + const renamebaz: string = ''; + + +; +() => (<>); +return { props: {...__sveltets_ensureRightProps<{exported1: string,exported2?: string,name1?: string,name2: string,renamed1?: string,renamed2: string}>(__sveltets_any("") as $$Props), ...__sveltets_ensureRightProps>({exported1: exported1,exported2: exported2,name1: name1,name2: name2,renamed1: rename1,renamed2: rename2}), ...{} as unknown as $$Props, ...{Foo: Foo, bar: bar, baz: baz, RenamedFoo: RenameFoo, renamedbar: renamebar, renamedbaz: renamebaz} as {Foo?: typeof Foo,bar?: typeof bar,baz?: string,RenamedFoo?: typeof RenameFoo,renamedbar?: typeof renamebar,renamedbaz?: string}}, slots: {}, getters: {}, events: {} }} + +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_with_any_event(render())) { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/input.svelte new file mode 100644 index 000000000..1024befec --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-interface/input.svelte @@ -0,0 +1,30 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/expected.tsx new file mode 100644 index 000000000..993a4e94b --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/expected.tsx @@ -0,0 +1,37 @@ +/// +<>;function render() { + + + interface $$Props { + exported1: string; + exported2?: string; + name1?: string; + name2: string; + renamed1?: string; + renamed2: string; + } + + let exported1: string; + let exported2: string = '';exported2 = __sveltets_any(exported2);; + + let name1: string = "world" + let name2: string; + + let rename1: string = ''; + let rename2: string; + + class Foo {} + function bar() {} + const baz: string = ''; + + class RenameFoo {} + function renamebar() {} + const renamebaz: string = ''; + + +; +() => (<>); +return { props: {...__sveltets_ensureRightProps<{exported1: string,exported2?: string,name1?: string,name2: string,renamed1?: string,renamed2: string}>(__sveltets_any("") as $$Props), ...__sveltets_ensureRightProps>({exported1: exported1,exported2: exported2,name1: name1,name2: name2,renamed1: rename1,renamed2: rename2}), ...{} as unknown as $$Props, ...{Foo: Foo, bar: bar, baz: baz, RenamedFoo: RenameFoo, renamedbar: renamebar, renamedbaz: renamebaz} as {Foo?: typeof Foo,bar?: typeof bar,baz?: string,RenamedFoo?: typeof RenameFoo,renamedbar?: typeof renamebar,renamedbaz?: string}}, slots: {}, getters: {}, events: {} }} + +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_with_any_event(render())) { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/input.svelte new file mode 100644 index 000000000..422f7c218 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$Props-type/input.svelte @@ -0,0 +1,30 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx index 376f8b38a..9f29d5ec4 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx @@ -110,6 +110,7 @@ declare function __sveltets_ensureTransition(transitionCall: SvelteTransitionRet declare function __sveltets_ensureFunction(expression: (e: Event & { detail?: any }) => unknown ): {}; declare function __sveltets_ensureType(type: AConstructorTypeOf, el: T): {}; declare function __sveltets_createEnsureSlot>>(): (k1: K1, k2: K2, val: Slots[K1][K2]) => Slots[K1][K2]; +declare function __sveltets_ensureRightProps(props: Props): {}; declare function __sveltets_cssProp(prop: Record): {}; declare function __sveltets_ctorOf(type: T): AConstructorTypeOf; declare function __sveltets_instanceOf(type: AConstructorTypeOf): T; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx index 5c295ae54..683061994 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-creates-dts/expected.tsx @@ -110,6 +110,7 @@ declare function __sveltets_ensureTransition(transitionCall: SvelteTransitionRet declare function __sveltets_ensureFunction(expression: (e: Event & { detail?: any }) => unknown ): {}; declare function __sveltets_ensureType(type: AConstructorTypeOf, el: T): {}; declare function __sveltets_createEnsureSlot>>(): (k1: K1, k2: K2, val: Slots[K1][K2]) => Slots[K1][K2]; +declare function __sveltets_ensureRightProps(props: Props): {}; declare function __sveltets_cssProp(prop: Record): {}; declare function __sveltets_ctorOf(type: T): AConstructorTypeOf; declare function __sveltets_instanceOf(type: AConstructorTypeOf): T; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/expected.tsx new file mode 100644 index 000000000..7a5d2b2d4 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/expected.tsx @@ -0,0 +1,24 @@ +/// +<>;function render() { + + let name1: string = "world" + let name2: string; + + let rename1: string = ''; + let rename2: string; + + class Foo {} + function bar() {} + const baz: string = ''; + + class RenameFoo {} + function renamebar() {} + const renamebaz: string = ''; + + +; +() => (<>); +return { props: {name1: name1 , name2: name2 , renamed1: rename1 , renamed2: rename2 , Foo: Foo , bar: bar , baz: baz , RenamedFoo: RenameFoo , renamedbar: renamebar , renamedbaz: renamebaz} as {name1?: string, name2: string, renamed1?: string, renamed2: string, Foo?: typeof Foo, bar?: typeof bar, baz?: string, RenamedFoo?: typeof RenameFoo, renamedbar?: typeof renamebar, renamedbaz?: string}, slots: {}, getters: {}, events: {} }} + +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_with_any_event(render())) { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/input.svelte new file mode 100644 index 000000000..97aaf2197 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list/input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-attr-present/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-attr-present/expected.tsx index b2ce72bce..e21bdf959 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-attr-present/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-attr-present/expected.tsx @@ -11,7 +11,7 @@ ); return { props: {foo: foo , foo2: foo2 , class: clazz , bar: bar}, slots: {}, getters: {bar: bar}, events: {} }} -export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['foo','foo2','class','bar'], __sveltets_with_any_event(render()))) { +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['foo','foo2','bar'], __sveltets_with_any_event(render()))) { get bar() { return render().getters.bar } get foo() { return render().props.foo } /**accessor*/ diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-mustachetag-true/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-mustachetag-true/expected.tsx index b8d03c745..c63e2592d 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-mustachetag-true/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/uses-accessors-mustachetag-true/expected.tsx @@ -11,7 +11,7 @@ ); return { props: {foo: foo , foo2: foo2 , class: clazz , bar: bar}, slots: {}, getters: {bar: bar}, events: {} }} -export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['foo','foo2','class','bar'], __sveltets_with_any_event(render()))) { +export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(['foo','foo2','bar'], __sveltets_with_any_event(render()))) { get bar() { return render().getters.bar } get foo() { return render().props.foo } /**accessor*/ From 6ac462160368ea3914f95e628240abf005146808 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 19 Jun 2021 12:03:21 +0200 Subject: [PATCH 2/4] throw when used in module script tag --- .../svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts index 02d835951..b1728b912 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts @@ -7,6 +7,7 @@ import { Generics } from './nodes/Generics'; import { is$$EventsDeclaration } from './nodes/ComponentEvents'; import { throwError } from './utils/error'; import { is$$SlotsDeclaration } from './nodes/slot'; +import { is$$PropsDeclaration } from './nodes/ExportedNames'; export function processModuleScriptTag( str: MagicString, @@ -32,6 +33,7 @@ export function processModuleScriptTag( generics.throwIfIsGeneric(node); throwIfIs$$EventsDeclaration(node, str, astOffset); throwIfIs$$SlotsDeclaration(node, str, astOffset); + throwIfIs$$PropsDeclaration(node, str, astOffset); ts.forEachChild(node, (n) => walk(n)); }; @@ -84,6 +86,12 @@ function throwIfIs$$SlotsDeclaration(node: ts.Node, str: MagicString, astOffset: } } +function throwIfIs$$PropsDeclaration(node: ts.Node, str: MagicString, astOffset: number) { + if (is$$PropsDeclaration(node)) { + throw$$Error(node, str, astOffset, '$$Props'); + } +} + function throw$$Error(node: ts.Node, str: MagicString, astOffset: number, type: string) { throwError( node.getStart() + astOffset, From b54e021e454d11131ee10f4d0e683c61b8294d0f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 19 Jun 2021 12:11:15 +0200 Subject: [PATCH 3/4] fix generics unused diagnostic --- .../plugins/typescript/features/DiagnosticsProvider.test.ts | 6 ++++++ .../testfiles/diagnostics/$$generic-unused.svelte | 5 +++++ packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts | 2 +- .../test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx | 2 +- .../test/svelte2tsx/samples/ts-$$generics/expected.tsx | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$generic-unused.svelte 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 48040ca7f..8dbe87a29 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -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); diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$generic-unused.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$generic-unused.svelte new file mode 100644 index 000000000..c49d4f2d9 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/$$generic-unused.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts index 40ccc2f3e..d95941289 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts @@ -47,7 +47,7 @@ export class Generics { toDefinitionString(addIgnore = false) { const surround = addIgnore ? surroundWithIgnoreComments : (str: string) => str; - return this.definitions.length ? `<${surround(this.definitions.join(','))}>` : ''; + return this.definitions.length ? surround(`<${this.definitions.join(',')}>`) : ''; } toReferencesString() { diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx index 9f29d5ec4..0c7346aca 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-dts/expected.tsx @@ -207,7 +207,7 @@ declare function __sveltets_unwrapPromiseLike(promise: PromiseLike | T): T import { createEventDispatcher } from 'svelte'; -function render() { +function render/*Ωignore_startΩ*//*Ωignore_endΩ*/() { diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics/expected.tsx index a5dd1d023..720e9cd04 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics/expected.tsx @@ -1,7 +1,7 @@ /// <>; import { createEventDispatcher } from 'svelte'; -function render() { +function render/*Ωignore_startΩ*//*Ωignore_endΩ*/() { From 95db0c12ced93457159e2e667b9cf28175b0b788 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 19 Jun 2021 12:12:13 +0200 Subject: [PATCH 4/4] lint --- packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index 48d7c31e5..cbec613f8 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -153,7 +153,7 @@ export class ExportedNames extends Map implements IExporte } private createReturnElements( - names: [string, ExportedName][], + names: Array<[string, ExportedName]>, dontAddTypeDef: boolean ): string[] { return names.map(([key, value]) => { @@ -164,7 +164,7 @@ export class ExportedNames extends Map implements IExporte }); } - private createReturnElementsType(names: [string, ExportedName][]) { + private createReturnElementsType(names: Array<[string, ExportedName]>) { return names.map(([key, value]) => { const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${ value.required ? '' : '?'