Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve attr tag type gen #328

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/popular-olives-dream.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
8 | </>
9 |
> 10 | <@item>
| ^^^^^ Type '{ renderBody: () => MarkoReturn<void>; [Symbol.iterator]: any; }' is not assignable to type 'AttrTag<{ x: number; renderBody?: Body<[], void> | undefined; }>'.
| ^^^^^ Type '{ renderBody: () => MarkoReturn<void>; [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<void>; [Symbol.iterator]: any; }' but required in type '{ x: number; renderBody?: Body<[], void> | undefined; }'.
11 | Hello!
12 | </>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Hovers
### Ln 4, Col 11
```marko
2 | <@a>
3 | <for of=[1, 2, 3]>
> 4 | <@b size="small"/>
| ^ (property) "size": "small"
5 | // ^?
6 | </for>
7 | </>
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Input {
a?: Marko.AttrTag<{
b?: Marko.AttrTag<{
size?: "small" | "large"
}>
}>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<list>
<@a>
<for of=[1, 2, 3]>
<@b size="small"/>
// ^?
</for>
</>
</list>
56 changes: 40 additions & 16 deletions packages/language-tools/marko.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ declare global {
override: Override,
): [0] extends [1 & Override] ? Marko.Global : Override;

export function attrTagNames<Input, Keys extends keyof Input & string>(
export function attrTagNames<Tag>(
tag: Tag,
fn: (input: AttrTagNames<Marko.Input<Tag>>) => void,
): void;
export function nestedAttrTagNames<Input>(
input: Input,
): Record<string, never> & {
[Key in Keys as `@${Input[Key] extends infer Value
? Value extends Marko.AttrTag<any>
? Key
: never
: never}`]: Input[Key];
};
fn: (input: AttrTagNames<Input>) => void,
): void;

export const rendered: {
scopes: Record<number, never>;
Expand Down Expand Up @@ -99,7 +98,8 @@ declare global {
tags: Tags,
index: Index,
tag: Tag,
): asserts tags is Tags & Record<Index, Tag>;
): asserts tags is Tags &
Record<Index, [0] extends [1 & Tag] ? any : Tag>;

export function assertRendered<Index extends number, Rendered, Result>(
rendered: Rendered,
Expand Down Expand Up @@ -324,16 +324,18 @@ declare global {
export function mergeAttrTags<Attrs extends readonly any[]>(
...attrs: Attrs
): MergeAttrTags<Attrs>;
export function attrTags<T>(
type?: T,
export function attrTag<AttrTag>(attrTags: AttrTag[]): AttrTag;
export function attrTagFor<Tag, Path extends readonly string[]>(
tag: Tag,
...path: Path
): <AttrTag>(
attrTags: Relate<
AttrTag,
[0] extends [1 & T]
? Marko.AttrTag<unknown>
: T extends Marko.AttrTag<infer Type>
? Marko.AttrTag<Type>
: Marko.AttrTag<unknown>
Marko.Input<Tag> extends infer Input
? [0] extends [1 & Input]
? Marko.AttrTag<unknown>
: AttrTagValue<Marko.Input<Tag>, Path>
: Marko.AttrTag<unknown>
>[],
) => AttrTag;

Expand Down Expand Up @@ -548,6 +550,28 @@ type AttrTagByObjectSize<
CheckNever<KnownKeys, Marko.AttrTag<Item>, undefined | Marko.AttrTag<Item>>
>;

type AttrTagValue<Input, Path extends readonly string[]> = Path extends [
infer Prop extends keyof Input,
...infer Rest extends readonly string[],
]
? Input[Prop] & Marko.AttrTag<unknown> extends infer Value
? Rest extends readonly []
? Value
: AttrTagValue<Value, Rest>
: Marko.AttrTag<unknown>
: Marko.AttrTag<unknown>;

type AttrTagNames<Input> = Record<string, never> &
(Input extends infer Input extends {}
? {
[Key in keyof Input & string as `@${Input[Key] extends infer Value
? Value extends Marko.AttrTag<any>
? Key
: never
: never}`]: Input[Key];
}
: never);

type RecordKeys<T> = keyof {
[K in keyof T as CheckNever<T[K], never, K>]: 0;
};
Expand Down
108 changes: 74 additions & 34 deletions packages/language-tools/src/extractors/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ type IfTagAlternate = {
};
};
type IfTagAlternates = Repeatable<IfTagAlternate>;
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.
Expand Down Expand Up @@ -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}]))`,
);
}
}
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -1072,27 +1083,7 @@ constructor(_?: Return) {}

#writeStaticAttrTags(
staticAttrTags: Exclude<ProcessedBody["staticAttrTags"], undefined>,
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;
Expand All @@ -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;
}
Expand All @@ -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")}([`);
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading