Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script setup lang="ts">
function childLocal(): string { return 'c'; }
</script>
<template><b>{{ childLocal() }}</b></template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
function legacyInit(): number { return 0; }
export default { name: 'Legacy', data() { return { n: legacyInit() }; } };
</script>
<template><i>legacy</i></template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script setup lang="ts">
defineProps<{ count: number }>();
function widgetLabel(): string { return 'w'; }
</script>
<template><span>{{ widgetLabel() }}</span></template>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function Badge(): string { return 'b'; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
function storeInit(): number { return 1; }
export default { value: storeInit() };
5 changes: 5 additions & 0 deletions graph/test/typescript/cases/81-vue-component-tag/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Parent from './views/Parent.vue';

export function mount(): unknown {
return Parent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.vue' {
const component: unknown;
export default component;
}
10 changes: 10 additions & 0 deletions graph/test/typescript/cases/81-vue-component-tag/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true
},
"include": ["**/*.ts", "**/*.vue"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import Child from '../components/Child.vue';
import MyWidget from '../components/MyWidget.vue';
import Legacy from '../components/Legacy.vue';
import { Badge } from '../components/badge';
import Store from '../components/store';
</script>
<template>
<Child />
<my-widget :count="1" />
<Legacy></Legacy>
<Badge />
<Store />
<div>{{ Store }}</div>
</template>
4 changes: 2 additions & 2 deletions graph/test/typescript/expected/80-vue-sfc.edges
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
ambiguous_unknown METHOD_CALL child#format(number) @L10 -> -
known_edge FUNCTION_CALL Comp.vue#<module-init>() @L10 -> Comp.vue#sfcHelper()
known_edge FUNCTION_CALL Comp.vue#<module-init>() @L10 -> child#Child({ msg: string })
known_edge FUNCTION_CALL Comp.vue#<module-init>() @L11 -> Comp.vue#onSave(number)
known_edge FUNCTION_CALL Comp.vue#<module-init>() @L11 -> child#format(number)
known_edge FUNCTION_CALL TemplateFirst.vue#<module-init>() @L3 -> TemplateFirst.vue#shown()
known_edge FUNCTION_CALL TemplateFirst.vue#<module-init>() @L3 -> child#MyBadge()
known_edge FUNCTION_CALL TemplateFirst.vue#<module-init>() @L5 -> TemplateFirst.vue#onInput()
known_edge JSX_COMPONENT_CALL Comp.vue#<module-init>() @L10 -> child#Child({ msg: string })
known_edge JSX_COMPONENT_CALL TemplateFirst.vue#<module-init>() @L3 -> child#MyBadge()
10 changes: 10 additions & 0 deletions graph/test/typescript/expected/81-vue-component-tag.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ambiguous_unknown FUNCTION_CALL components/MyWidget.vue#<module-init>() @L2 -> -
ambiguous_unknown JSX_COMPONENT_CALL views/Parent.vue#<module-init>() @L13 -> -
known_edge FUNCTION_CALL components/Child.vue#<module-init>() @L4 -> components/Child.vue#childLocal()
known_edge FUNCTION_CALL components/Legacy.vue#data() @L3 -> components/Legacy.vue#legacyInit()
known_edge FUNCTION_CALL components/MyWidget.vue#<module-init>() @L5 -> components/MyWidget.vue#widgetLabel()
known_edge FUNCTION_CALL components/store#<module-init>() @L2 -> components/store#storeInit()
known_edge JSX_COMPONENT_CALL views/Parent.vue#<module-init>() @L10 -> components/MyWidget.vue#<module-init>()
known_edge JSX_COMPONENT_CALL views/Parent.vue#<module-init>() @L11 -> components/Legacy.vue#<module-init>()
known_edge JSX_COMPONENT_CALL views/Parent.vue#<module-init>() @L12 -> components/badge#Badge()
known_edge JSX_COMPONENT_CALL views/Parent.vue#<module-init>() @L9 -> components/Child.vue#<module-init>()
4 changes: 4 additions & 0 deletions graph/test/typescript/expected/81-vue-component-tag.entries
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
── entry_point (3) ──
exported_from_entry_module main#mount main.ts:3
unimported_module main#<module> main.ts:1
unimported_module shims-vue#<module> shims-vue.d.ts:1
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
81-vue-component-tag [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
8 changes: 8 additions & 0 deletions graph/test/typescript/expected/81-vue-component-tag.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
oracle=1 engine=8 agree=1 missing=0 (known 0, NEW 0) extra=7
extra components/Child.vue#<module-init>() -> components/Child.vue#childLocal()
extra components/Legacy.vue#data() -> components/Legacy.vue#legacyInit()
extra components/MyWidget.vue#<module-init>() -> components/MyWidget.vue#widgetLabel()
extra views/Parent.vue#<module-init>() -> components/Child.vue#<module-init>()
extra views/Parent.vue#<module-init>() -> components/Legacy.vue#<module-init>()
extra views/Parent.vue#<module-init>() -> components/MyWidget.vue#<module-init>()
extra views/Parent.vue#<module-init>() -> components/badge#Badge()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
81-vue-component-tag [types]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
sites 0 resolved 0
tiers
contexts
not scored: 0 rows whose target is not a client declaration
18 changes: 18 additions & 0 deletions graph/typescript/engine/expression-resolution/callee-resolution.dl
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,24 @@ expr_call_candidate(ce, m) :- call_site("client", "JSX_COMPONENT_CALL", cn, _, _
import_binding("client", "DEFAULT", cn, _, mod, ih),
import_target_module(ih, "client", mod2),
jsx_default_export_renders(mod2, m).
// A Vue single-file component, `import Child from "./Child.vue"` rendered as `<Child/>`
// (a tag of a `.vue` template, or JSX). Its script is the module's top-level code — a
// `<script setup>` body runs once per instance, and the virtual script the parser reads
// writes the component's template there too — so the tag reaches the module initializer.
// A `<script setup>` component has no default export at all; one whose default export
// renders (a wrapper, above) is reached through that export instead.
jsx_vue_module(mod) :- module_decl("client", _, _, fp, _, mod),
match(".*[.]vue", fp).
jsx_vue_default_renders(mod) :- jsx_default_export_renders(mod, _).
expr_call_candidate(ce, init) :- call_site("client", "JSX_COMPONENT_CALL", cn, _, _, ce, cs),
cn != "",
call_module(cs, mod),
!callee_names_local(ce),
import_binding("client", "DEFAULT", cn, _, mod, ih),
import_target_module(ih, "client", mod2),
jsx_vue_module(mod2),
!jsx_vue_default_renders(mod2),
module_init("client", init, mod2).
// A LOADER — `lazy(() => import("./Settings"))`, Next.js `dynamic(...)`, `loadable(...)`.
// Argument 0 is not the component: it returns a module, or a promise of one, and what
// renders is the component that settles out of it.
Expand Down
2 changes: 2 additions & 0 deletions graph/typescript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@
.decl jsx_vue_definer_name(c0:symbol)
.decl jsx_vue_definer(c0:symbol)
.decl jsx_vue_has_setup(c0:symbol)
.decl jsx_vue_module(c0:symbol)
.decl jsx_vue_default_renders(c0:symbol)
.decl lib_boundary_reason(c0:symbol,c1:symbol)
.decl lib_capped()
.decl lib_export_relative_base(c0:symbol,c1:symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TsTypeRefContext, TsReferenceOwnerKind } from '@/enums/typescript/type-
import { nodeId } from '@/parsers/typescript/extractors/ts-binder';
import { TsTypeReferenceExtractor } from '@/parsers/typescript/extractors/ts-type-reference-extractor';
import { EntityUtils } from '@/utils/entity-utils';
import { isVueTemplateTag } from '@/utils/vue-sfc';

/**
* Emits `ts_expression` and `ts_call_site` — schema §4.14, §4.15.
Expand Down Expand Up @@ -986,6 +987,11 @@ function callKindOf(node: ts.Node, callee: ts.Node | undefined): TsCallKind {
? TsCallKind.METHOD_CALL
: TsCallKind.JSX_COMPONENT_CALL;
}
// A `.vue` template tag, `<Child/>`, is written into the virtual script as a
// marked call; it is the same render a JSX tag is.
if (isVueTemplateTag(node) && callee && ts.isIdentifier(callee)) {
return TsCallKind.JSX_COMPONENT_CALL;
}
// Checked before the callee shape, because `@a.b.Get("/x")` is a decorator
// call first and a property-access callee second. Which one wins decides
// whether an engine sees a call that happens at class-definition time.
Expand Down
26 changes: 24 additions & 2 deletions parser/src/utils/vue-sfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { isJsComponentFile, sfcScriptText } from '@/utils/javascript';
*
* - each inline `<script>` block's body, verbatim, at its own line and column;
* - one top-level statement per template reference, on the line it is written:
* `<Child/>` and `<my-child>` -> `Child();` / `MyChild();` (a JSX tag is a call too)
* `<Child/>` and `<my-child>` -> `Child(/*<tag>*\/);` / `MyChild(/*<tag>*\/);`
* (a JSX tag is a call too; see VUE_TAG_MARKER)
* `@ping="onPing"` -> `onPing();` (Vue calls a bare handler)
* `@ping="count++; log(x)"` -> `count++; log(x);`
* `:x="fmt(y)"`, `v-if="ok"` -> `(fmt(y));`
Expand Down Expand Up @@ -318,7 +319,7 @@ function readTag(
const tagName = nameMatch[1]!;
const component = componentBinding(tagName);
if (component !== undefined) {
emit(lt + 1, `${component}();`);
emit(lt + 1, `${component}(${VUE_TAG_MARKER});`);
}
let i = lt + nameMatch[0].length;
while (i < end) {
Expand Down Expand Up @@ -405,6 +406,27 @@ function attributeReference(
emit(valueStart, `(${value});`);
}

/**
* Written inside a tag's call, `Child(/*<tag>*\/)`: what makes the call a TAG
* rather than a template expression that calls a function, so it is recorded as
* a component render (a JSX tag's call kind) and resolves the way `<Child/>` in
* TSX does, default imports of components included.
*/
export const VUE_TAG_MARKER = '/*<tag>*/';

/** Whether `call` is a template tag of a `.vue` component's virtual script. */
export function isVueTemplateTag(call: ts.Node): boolean {
if (!ts.isCallExpression(call) || call.arguments.length !== 0) {
return false;
}
const sf = call.getSourceFile();
if (!isVueFile(sf.fileName)) {
return false;
}
const text = sf.text;
return text.slice(call.expression.end, call.end).replace(/\s/g, '') === `(${VUE_TAG_MARKER})`;
}

/** The binding a tag names, or `undefined` for an HTML element or a Vue built-in. */
function componentBinding(tagName: string): string | undefined {
if (VUE_BUILT_IN_TAGS.has(tagName)) {
Expand Down
Loading