diff --git a/.changeset/great-cars-poke.md b/.changeset/great-cars-poke.md new file mode 100644 index 00000000..e0a8ac54 --- /dev/null +++ b/.changeset/great-cars-poke.md @@ -0,0 +1,7 @@ +--- +"@marko/language-server": patch +"@marko/type-check": patch +"marko-vscode": patch +--- + +Improve type inference for repeated attr tags diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tag-target-property/__snapshots__/attr-tag-target-property.expected/index.md b/packages/language-server/src/__tests__/fixtures/script/attr-tag-target-property/__snapshots__/attr-tag-target-property.expected/index.md index 5e1aa342..ed62c5ae 100644 --- a/packages/language-server/src/__tests__/fixtures/script/attr-tag-target-property/__snapshots__/attr-tag-target-property.expected/index.md +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tag-target-property/__snapshots__/attr-tag-target-property.expected/index.md @@ -14,3 +14,15 @@ 9 | ``` +### Ln 10, Col 4 +```marko + 8 | + 9 | +> 10 | <@item> + | ^^^^^ Type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' is not assignable to type 'AttrTag<{ x: number; renderBody?: Body<[], void> | undefined; }>'. + Property 'x' is missing in type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' but required in type '{ x: number; renderBody?: Body<[], void> | undefined; }'. + 11 | Hello! + 12 | + 13 | +``` + diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/__snapshots__/attr-tags-params-js.expected/components/child.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/__snapshots__/attr-tags-params-js.expected/components/child.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/__snapshots__/attr-tags-params-js.expected/index.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/__snapshots__/attr-tags-params-js.expected/index.md new file mode 100644 index 00000000..30d21835 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/__snapshots__/attr-tags-params-js.expected/index.md @@ -0,0 +1,40 @@ +## Hovers +### Ln 6, Col 7 +```marko + 4 | <@foo bar/> + 5 | <@foo|data|> +> 6 | ${data} + | ^ (parameter) data: { + a: string; + b: number; +} + 7 | //^? + 8 | + 9 | +``` + +### Ln 14, Col 7 +```marko + 12 | <@foo bar/> + 13 | <@foo|data|> +> 14 | ${data} + | ^ (parameter) data: { + a: string; + b: number; +} + 15 | //^? + 16 | + 17 | +``` + +### Ln 23, Col 7 +```marko + 21 | <@foo bar/> + 22 | <@foo|data|> +> 23 | ${data} + | ^ (parameter) data: any + 24 | //^? + 25 | + 26 | +``` + diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/components/child.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/components/child.marko new file mode 100644 index 00000000..44e16d98 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/components/child.marko @@ -0,0 +1,8 @@ +/** + * @typedef {{ + * foo: Marko.AttrTag<{ + * bar?: boolean; + * renderBody?: Marko.Body<[{a: string, b: number}]> + * }> + * }} Input + */ diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/index.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/index.marko new file mode 100644 index 00000000..4fc0f64e --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/index.marko @@ -0,0 +1,26 @@ +import Child from ""; + + + <@foo bar/> + <@foo|data|> + ${data} + //^? + + + + + <@foo bar/> + <@foo|data|> + ${data} + //^? + + + +// This errors for now, because `typeof (some + expression)` is not allowed +<${true && Child}> + <@foo bar/> + <@foo|data|> + ${data} + //^? + + \ No newline at end of file diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/marko.json b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/marko.json new file mode 100644 index 00000000..4400c3a3 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params-js/marko.json @@ -0,0 +1,3 @@ +{ + "script-lang": "js" +} diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/__snapshots__/attr-tags-params.expected/components/child.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/__snapshots__/attr-tags-params.expected/components/child.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/__snapshots__/attr-tags-params.expected/index.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/__snapshots__/attr-tags-params.expected/index.md new file mode 100644 index 00000000..e5d168f6 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/__snapshots__/attr-tags-params.expected/index.md @@ -0,0 +1,52 @@ +## Hovers +### Ln 6, Col 7 +```marko + 4 | <@foo bar/> + 5 | <@foo|data|> +> 6 | ${data} + | ^ (parameter) data: { + a: string; + b: number; +} + 7 | //^? + 8 | + 9 | +``` + +### Ln 14, Col 7 +```marko + 12 | <@foo bar/> + 13 | <@foo|data|> +> 14 | ${data} + | ^ (parameter) data: { + a: string; + b: number; +} + 15 | //^? + 16 | + 17 | +``` + +### Ln 23, Col 7 +```marko + 21 | <@foo bar/> + 22 | <@foo|data|> +> 23 | ${data} + | ^ (parameter) data: any + 24 | //^? + 25 | + 26 | +``` + +## Diagnostics +### Ln 22, Col 9 +```marko + 20 | <${true && Child}> + 21 | <@foo bar/> +> 22 | <@foo|data|> + | ^^^^ Parameter 'data' implicitly has an 'any' type. + 23 | ${data} + 24 | //^? + 25 | +``` + diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/components/child.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/components/child.marko new file mode 100644 index 00000000..b1a1b8ce --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/components/child.marko @@ -0,0 +1,6 @@ +export interface Input { + foo: Marko.AttrTag<{ + bar?: boolean; + renderBody?: Marko.Body<[{a: string, b: number}]> + }> +} diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/index.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/index.marko new file mode 100644 index 00000000..4fc0f64e --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-params/index.marko @@ -0,0 +1,26 @@ +import Child from ""; + + + <@foo bar/> + <@foo|data|> + ${data} + //^? + + + + + <@foo bar/> + <@foo|data|> + ${data} + //^? + + + +// This errors for now, because `typeof (some + expression)` is not allowed +<${true && Child}> + <@foo bar/> + <@foo|data|> + ${data} + //^? + + \ No newline at end of file diff --git a/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/__snapshots__/recursive-input-scope-hoist.expected/index.md b/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/__snapshots__/recursive-input-scope-hoist.expected/index.md index e0ee4f8c..a939bdc5 100644 --- a/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/__snapshots__/recursive-input-scope-hoist.expected/index.md +++ b/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/__snapshots__/recursive-input-scope-hoist.expected/index.md @@ -1,4 +1,15 @@ ## Hovers +### Ln 3, Col 7 +```marko + 1 | + 2 | <@comment#a> +> 3 | <@comment#b> + | ^ (property) "@comment": Marko.AttrTag | undefined + 4 | // ^? + 5 | + 6 | +``` + ### Ln 15, Col 3 ```marko 13 | @@ -31,16 +42,3 @@ 21 | }/> ``` -## Diagnostics -### Ln 2, Col 4 -```marko - 1 | -> 2 | <@comment#a> - | ^^^^^^^^ Type '{ comment: { id: string; renderBody: () => MarkoReturn; [Symbol.iterator]: any; }; renderBody: () => MarkoReturn; [Symbol.iterator]: any; id: string; } | { renderBody: () => MarkoReturn<...>; [Symbol.iterator]: any; }' is not assignable to type 'AttrTag'. - Type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' is not assignable to type 'AttrTag'. - Property 'id' is missing in type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' but required in type 'Comment'. - 3 | <@comment#b> - 4 | // ^? - 5 | -``` - diff --git a/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/index.marko b/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/index.marko index edaa6085..1837ddf5 100644 --- a/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/index.marko +++ b/packages/language-server/src/__tests__/fixtures/script/recursive-input-scope-hoist/index.marko @@ -6,7 +6,7 @@ - <@comment> + <@comment#c> diff --git a/packages/language-tools/src/extractors/script/index.ts b/packages/language-tools/src/extractors/script/index.ts index e9137185..d26361ab 100644 --- a/packages/language-tools/src/extractors/script/index.ts +++ b/packages/language-tools/src/extractors/script/index.ts @@ -695,6 +695,7 @@ constructor(_?: Return) {} #writeTag(tag: Node.Tag) { const tagName = tag.nameText; const renderId = this.#getRenderId(tag); + let nestedTagType: string | undefined; if (renderId) { this.#extractor.write( @@ -709,6 +710,10 @@ constructor(_?: Return) {} if (def) { const importPath = resolveTagImport(this.#filename, def); + const isMarkoFile = importPath?.endsWith(".marko"); + if (isMarkoFile) { + nestedTagType = `import("${importPath}").Input`; + } const renderer = importPath?.endsWith(".marko") ? `renderTemplate(import("${importPath}"))` : def.html @@ -727,6 +732,7 @@ constructor(_?: Return) {} this.#extractor.write(varShared(renderer)); } } else if (REG_TAG_NAME_IDENTIFIER.test(tagName)) { + nestedTagType = `Marko.Input`; this.#extractor .write(`${varShared("renderDynamicTag")}(\n`) .copy(tag.name) @@ -746,7 +752,7 @@ constructor(_?: Return) {} this.#extractor.write("()()("); } - this.#writeTagInputObject(tag); + this.#writeTagInputObject(tag, nestedTagType); if (renderId) { this.#extractor.write(`)`); @@ -1035,6 +1041,7 @@ constructor(_?: Return) {} #writeAttrTags( { staticAttrTags, dynamicAttrTagParents }: ProcessedBody, inMerge: boolean, + nestedTagType?: string, ) { let wasMerge = false; @@ -1051,7 +1058,7 @@ constructor(_?: Return) {} } if (staticAttrTags) { - this.#writeStaticAttrTags(staticAttrTags, inMerge); + this.#writeStaticAttrTags(staticAttrTags, inMerge, nestedTagType); if (dynamicAttrTagParents) this.#extractor.write(`}${SEP_COMMA_NEW_LINE}`); } @@ -1065,6 +1072,7 @@ constructor(_?: Return) {} #writeStaticAttrTags( staticAttrTags: Exclude, wasMerge: boolean, + nestedTagType?: string, ) { if (!wasMerge) this.#extractor.write("...{"); this.#extractor.write( @@ -1099,7 +1107,13 @@ constructor(_?: Return) {} this.#extractor.write("]: "); if (isRepeated) { - this.#extractor.write(`${varShared("repeatedAttrTag")}(\n`); + this.#extractor.write(`${varShared("repeatedAttrTag")}(\n...\n`); + if (nestedTagType && this.#scriptLang === ScriptLang.js) { + this.#extractor.write( + `/** @satisfies {${nestedTagType}["${name}"][]} */\n`, + ); + } + this.#extractor.write(`([\n`); } for (const childNode of attrTag) { @@ -1108,6 +1122,10 @@ constructor(_?: Return) {} } if (isRepeated) { + this.#extractor.write("])"); + if (nestedTagType && this.#scriptLang === ScriptLang.ts) { + this.#extractor.write(` satisfies ${nestedTagType}["${name}"][]`); + } this.#extractor.write(`)${SEP_COMMA_NEW_LINE}`); } } @@ -1194,7 +1212,7 @@ constructor(_?: Return) {} } } - #writeTagInputObject(tag: Node.ParentTag) { + #writeTagInputObject(tag: Node.ParentTag, nestedTagType?: string) { if (!tag.params) this.#writeComments(tag); let hasInput = false; @@ -1227,7 +1245,7 @@ constructor(_?: Return) {} let hasRenderBody = false; if (body) { hasInput = true; - this.#writeAttrTags(body, false); + this.#writeAttrTags(body, false, nestedTagType); hasRenderBody = body.renderBody !== undefined; } else if (tag.close) { hasRenderBody = true;