From 091726b9f6347e5c27284c12ecdfcf6e319f3f13 Mon Sep 17 00:00:00 2001 From: dpiercey Date: Wed, 8 Jan 2025 14:41:03 -0700 Subject: [PATCH] fix: improve attr tag type gen --- .changeset/popular-olives-dream.md | 8 ++ .../index.md | 2 +- .../components/list.md | 0 .../attr-tags-nested-type.expected/index.md | 12 ++ .../components/list.marko | 7 ++ .../script/attr-tags-nested-type/index.marko | 8 ++ packages/language-tools/marko.internal.d.ts | 56 ++++++--- .../src/extractors/script/index.ts | 108 ++++++++++++------ 8 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 .changeset/popular-olives-dream.md create mode 100644 packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/components/list.md create mode 100644 packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/index.md create mode 100644 packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/components/list.marko create mode 100644 packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/index.marko diff --git a/.changeset/popular-olives-dream.md b/.changeset/popular-olives-dream.md new file mode 100644 index 00000000..e801802a --- /dev/null +++ b/.changeset/popular-olives-dream.md @@ -0,0 +1,8 @@ +--- +"@marko/language-server": patch +"@marko/language-tools": patch +"@marko/type-check": patch +"marko-vscode": patch +--- + +Improve attribute tag type generation and completions. 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 ed62c5ae..0d144536 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 @@ -19,7 +19,7 @@ 8 | 9 | > 10 | <@item> - | ^^^^^ Type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' is not assignable to type 'AttrTag<{ x: number; renderBody?: Body<[], void> | undefined; }>'. + | ^^^^^ Type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' is not assignable to type '{ x: number; renderBody?: Body<[], void> | undefined; } & { [Symbol.iterator](): Iterator<{ x: number; renderBody?: Body<[], void> | undefined; }, any, any>; } & { ...; }'. Property 'x' is missing in type '{ renderBody: () => MarkoReturn; [Symbol.iterator]: any; }' but required in type '{ x: number; renderBody?: Body<[], void> | undefined; }'. 11 | Hello! 12 | diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/components/list.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/components/list.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/index.md b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/index.md new file mode 100644 index 00000000..1a719535 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/__snapshots__/attr-tags-nested-type.expected/index.md @@ -0,0 +1,12 @@ +## Hovers +### Ln 4, Col 11 +```marko + 2 | <@a> + 3 | +> 4 | <@b size="small"/> + | ^ (property) "size": "small" + 5 | // ^? + 6 | + 7 | +``` + diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/components/list.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/components/list.marko new file mode 100644 index 00000000..bbc46952 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/components/list.marko @@ -0,0 +1,7 @@ +export interface Input { + a?: Marko.AttrTag<{ + b?: Marko.AttrTag<{ + size?: "small" | "large" + }> + }> +} diff --git a/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/index.marko b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/index.marko new file mode 100644 index 00000000..4127a6e2 --- /dev/null +++ b/packages/language-server/src/__tests__/fixtures/script/attr-tags-nested-type/index.marko @@ -0,0 +1,8 @@ + + <@a> + + <@b size="small"/> + // ^? + + + \ No newline at end of file diff --git a/packages/language-tools/marko.internal.d.ts b/packages/language-tools/marko.internal.d.ts index cc8019f3..e54fa4b4 100644 --- a/packages/language-tools/marko.internal.d.ts +++ b/packages/language-tools/marko.internal.d.ts @@ -25,15 +25,14 @@ declare global { override: Override, ): [0] extends [1 & Override] ? Marko.Global : Override; - export function attrTagNames( + export function attrTagNames( + tag: Tag, + fn: (input: AttrTagNames>) => void, + ): void; + export function nestedAttrTagNames( input: Input, - ): Record & { - [Key in Keys as `@${Input[Key] extends infer Value - ? Value extends Marko.AttrTag - ? Key - : never - : never}`]: Input[Key]; - }; + fn: (input: AttrTagNames) => void, + ): void; export const rendered: { scopes: Record; @@ -99,7 +98,8 @@ declare global { tags: Tags, index: Index, tag: Tag, - ): asserts tags is Tags & Record; + ): asserts tags is Tags & + Record; export function assertRendered( rendered: Rendered, @@ -324,16 +324,18 @@ declare global { export function mergeAttrTags( ...attrs: Attrs ): MergeAttrTags; - export function attrTags( - type?: T, + export function attrTag(attrTags: AttrTag[]): AttrTag; + export function attrTagFor( + tag: Tag, + ...path: Path ): ( attrTags: Relate< AttrTag, - [0] extends [1 & T] - ? Marko.AttrTag - : T extends Marko.AttrTag - ? Marko.AttrTag - : Marko.AttrTag + Marko.Input extends infer Input + ? [0] extends [1 & Input] + ? Marko.AttrTag + : AttrTagValue, Path> + : Marko.AttrTag >[], ) => AttrTag; @@ -548,6 +550,28 @@ type AttrTagByObjectSize< CheckNever, undefined | Marko.AttrTag> >; +type AttrTagValue = Path extends [ + infer Prop extends keyof Input, + ...infer Rest extends readonly string[], +] + ? Input[Prop] & Marko.AttrTag extends infer Value + ? Rest extends readonly [] + ? Value + : AttrTagValue + : Marko.AttrTag + : Marko.AttrTag; + +type AttrTagNames = Record & + (Input extends infer Input extends {} + ? { + [Key in keyof Input & string as `@${Input[Key] extends infer Value + ? Value extends Marko.AttrTag + ? Key + : never + : never}`]: Input[Key]; + } + : never); + type RecordKeys = keyof { [K in keyof T as CheckNever]: 0; }; diff --git a/packages/language-tools/src/extractors/script/index.ts b/packages/language-tools/src/extractors/script/index.ts index 1c961300..d03f1cd9 100644 --- a/packages/language-tools/src/extractors/script/index.ts +++ b/packages/language-tools/src/extractors/script/index.ts @@ -65,6 +65,12 @@ type IfTagAlternate = { }; }; type IfTagAlternates = Repeatable; +type AttrTagTree = { + [x: string]: { + tags: Node.AttrTag[]; + nested?: AttrTagTree; + }; +}; // TODO: Dedupe taglib completions with TS completions. (typescript project ignore taglib completions) // TODO: special types for macro and tag tags. @@ -742,8 +748,16 @@ constructor(_?: Return) {} this.#writeDynamicTagName(tag); } + this.#extractor.write("\n)),"); + + const attrTagTree = this.#getAttrTagTree(tag); + if (attrTagTree) { + this.#writeAttrTagTree(attrTagTree, `${varShared("tags")}[${tagId}]`); + this.#extractor.write(","); + } + this.#extractor.write( - `\n)),${varShared(isDynamic ? "renderDynamicTag" : "renderTemplate")}(${varShared("tags")}[${tagId}]))`, + `${varShared(isDynamic ? "renderDynamicTag" : "renderTemplate")}(${varShared("tags")}[${tagId}]))`, ); } } @@ -1040,26 +1054,23 @@ constructor(_?: Return) {} return hasAttrs; } - #writeAttrTags( - { staticAttrTags, dynamicAttrTagParents }: ProcessedBody, - inMerge: boolean, - ) { + #writeAttrTags({ staticAttrTags, dynamicAttrTagParents }: ProcessedBody) { let wasMerge = false; if (dynamicAttrTagParents) { if (staticAttrTags) { this.#extractor.write(`...${varShared("mergeAttrTags")}({\n`); - inMerge = wasMerge = true; + wasMerge = true; } else if (dynamicAttrTagParents.length > 1) { this.#extractor.write(`...${varShared("mergeAttrTags")}(\n`); - inMerge = wasMerge = true; + wasMerge = true; } else { this.#extractor.write(`...`); } } if (staticAttrTags) { - this.#writeStaticAttrTags(staticAttrTags, inMerge); + this.#writeStaticAttrTags(staticAttrTags); if (dynamicAttrTagParents) this.#extractor.write(`}${SEP_COMMA_NEW_LINE}`); } @@ -1072,27 +1083,7 @@ constructor(_?: Return) {} #writeStaticAttrTags( staticAttrTags: Exclude, - wasMerge: boolean, ) { - if (!wasMerge) this.#extractor.write("...{"); - this.#extractor.write( - `[${varShared("never")}](){\nconst attrTags = ${varShared( - "attrTagNames", - )}(this);\n`, - ); - - for (const nameText in staticAttrTags) { - for (const tag of staticAttrTags[nameText]) { - this.#extractor.write(`attrTags["`); - this.#extractor.copy(tag.name); - this.#extractor.write('"];\n'); - } - } - - this.#extractor.write("\n}"); - if (!wasMerge) this.#extractor.write("}"); - this.#extractor.write(SEP_COMMA_NEW_LINE); - for (const nameText in staticAttrTags) { const attrTag = staticAttrTags[nameText]; const isRepeated = attrTag.length > 1; @@ -1106,11 +1097,11 @@ constructor(_?: Return) {} if (isRepeated) { const tagId = this.#tagIds.get(firstAttrTag.owner!); if (tagId) { - let accessor = `["${name}"]`; + let accessor = `"${name}"`; let curTag = firstAttrTag.parent; while (curTag) { if (curTag.type === NodeType.AttrTag) { - accessor = `["${this.#getAttrTagName(curTag)}"]${accessor}`; + accessor = `"${this.#getAttrTagName(curTag)}",${accessor}`; } else if (!isControlFlowTag(curTag)) { break; } @@ -1119,10 +1110,10 @@ constructor(_?: Return) {} } this.#extractor.write( - `${varShared("attrTags")}(${varShared("input")}(${varShared("tags")}[${tagId}])${accessor})([\n`, + `${varShared("attrTagFor")}(${varShared("tags")}[${tagId}],${accessor})([`, ); } else { - this.#extractor.write(`${varShared("attrTags")}()([\n`); + this.#extractor.write(`${varShared("attrTag")}([`); } } @@ -1265,7 +1256,7 @@ constructor(_?: Return) {} body = this.#processBody(tag); if (body) { hasInput = true; - this.#writeAttrTags(body, false); + this.#writeAttrTags(body); hasBodyContent = body.content !== undefined; } else if (tag.close) { hasBodyContent = true; @@ -1595,7 +1586,7 @@ constructor(_?: Return) {} this.#extractor.write("return "); } this.#extractor.write("{\n"); - this.#writeAttrTags(body, true); + this.#writeAttrTags(body); this.#extractor.write("}"); if (body.content) { @@ -1769,6 +1760,55 @@ constructor(_?: Return) {} ); } + #writeAttrTagTree(tree: AttrTagTree, valueExpression: string, nested?: true) { + this.#extractor.write( + `${varShared(nested ? "nestedAttrTagNames" : "attrTagNames")}(${valueExpression},input=>{\n`, + ); + for (const name in tree) { + const { tags, nested } = tree[name]; + for (const tag of tags) { + this.#extractor.write('input["').copy(tag.name).write('"];\n'); + } + + if (nested) { + this.#writeAttrTagTree(nested, `input["@${name}"]`, true); + } + } + this.#extractor.write(`})`); + } + + #getAttrTagTree(tag: Node.Tag | Node.AttrTag, attrTags?: AttrTagTree) { + if (!tag.hasAttrTags || !tag.body) return attrTags; + + const curAttrTags = attrTags || {}; + for (const child of tag.body) { + switch (child.type) { + case NodeType.AttrTag: { + const name = this.#getAttrTagName(child); + const prev = curAttrTags[name]; + + if (prev) { + prev.tags.push(child); + prev.nested = this.#getAttrTagTree(child, prev.nested); + } else { + curAttrTags[name] = { + tags: [child], + nested: this.#getAttrTagTree(child), + }; + } + break; + } + case NodeType.Tag: + if (isControlFlowTag(child)) { + this.#getAttrTagTree(child, curAttrTags); + } + break; + } + } + + return curAttrTags; + } + #testAtIndex(reg: RegExp, index: number) { reg.lastIndex = index; return reg.test(this.#code);