diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts b/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts new file mode 100644 index 000000000..036ca9181 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/lib/vue.ts @@ -0,0 +1,29 @@ +// The Vue entry points that matter, declared with their shapes. No Vue dependency. +declare global { + namespace JSX { + type Element = string; + interface ElementAttributesProperty { + props: unknown; + } + interface IntrinsicElements { + [name: string]: unknown; + } + } +} + +export interface ComponentOptions { + name?: string; + props?: Record; + setup?: (props: any) => unknown; + render?: () => string; +} +// A constructor type, as Vue's own DefineComponent is: nothing here says what renders. +export interface Component { + new (...args: any[]): { props: any }; +} +export declare function defineComponent(options: ComponentOptions): Component; +export declare function defineComponent(setup: (props: any) => () => string): Component; +export declare function defineNuxtComponent(options: ComponentOptions): Component; + +// NOT a component definer: it takes an options-shaped object and returns something else. +export declare function makeStore(options: ComponentOptions): Component; diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx new file mode 100644 index 000000000..0facbff37 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/app.tsx @@ -0,0 +1,26 @@ +import { defineComponent } from "../lib/vue"; +import Card from "./card"; +import { Child, Panel, FnForm, NuxtCard, Store } from "./parts"; +import { Button, Local } from "./reg"; + +function helper(): number { + return 42; +} + +export const App = defineComponent({ + setup() { + const onPing = () => helper(); + return () => ( +
+ + + + + + +
+ ); + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx new file mode 100644 index 000000000..b3626705b --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/button.tsx @@ -0,0 +1,12 @@ +import { defineComponent } from "../lib/vue"; + +export function leafButton(): number { + return 8; +} + +export default defineComponent({ + name: "Button", + setup() { + return () => {leafButton()}; + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx new file mode 100644 index 000000000..63e84e1a0 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/card.tsx @@ -0,0 +1,15 @@ +import { defineComponent } from "../lib/vue"; +import { Child } from "./parts"; + +export function leafCard(): number { + return 7; +} + +// the module's default export, rendered through a default import +export default defineComponent({ + name: "Card", + render() { + leafCard(); + return ; + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx new file mode 100644 index 000000000..1f4193a9d --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/parts.tsx @@ -0,0 +1,53 @@ +import { defineComponent, defineNuxtComponent, makeStore } from "../lib/vue"; + +export function leafSetup(): number { + return 1; +} +export function leafRendered(): number { + return 2; +} +export function leafRender(): number { + return 3; +} +export function leafFn(): number { + return 4; +} +export function leafNuxt(): number { + return 5; +} +export function leafStore(): number { + return 6; +} + +// setup() returns the render function +export const Child = defineComponent({ + props: { msg: String }, + setup(props) { + leafSetup(); + return () => String(leafRendered()); + }, +}); + +// the render option, no setup +export const Panel = defineComponent({ + name: "Panel", + render() { + return String(leafRender()); + }, +}); + +// the function form: the function IS setup +export const FnForm = defineComponent((props) => () => String(leafFn())); + +export const NuxtCard = defineNuxtComponent({ + setup() { + return () => String(leafNuxt()); + }, +}); + +// CONTROL: the same object handed to another function is not a component +export const Store = makeStore({ + setup() { + return () => String(leafStore()); + }, +}); diff --git a/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts new file mode 100644 index 000000000..ab73a3cf4 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-definecomponent-jsx/src/reg.ts @@ -0,0 +1,24 @@ +import { defineComponent } from "../lib/vue"; +import _Button from "./button"; + +export function leafLocal(): number { + return 9; +} + +// the registration idiom: a component library re-exports each component through an +// installer that returns its argument +export function withInstall(component: T): T { + return component; +} + +// …of a component default-imported from its own module +export const Button = withInstall(_Button); + +// …and of one defined in this module +const _Local = defineComponent({ + setup() { + leafLocal(); + return () => "local"; + }, +}); +export const Local = withInstall(_Local); diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges new file mode 100644 index 000000000..332f06303 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges @@ -0,0 +1,36 @@ +ambiguous_unknown FUNCTION_CALL app#() @L10 -> - +ambiguous_unknown FUNCTION_CALL button#() @L7 -> - +ambiguous_unknown FUNCTION_CALL card#() @L9 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L27 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L44 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L51 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L23 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L32 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L42 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L49 -> - +ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - +ambiguous_unknown FUNCTION_CALL reg#() @L18 -> - +ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - +callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() +known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() +known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() +known_edge FUNCTION_CALL parts#() @L27 -> parts#leafRendered() +known_edge FUNCTION_CALL parts#() @L40 -> parts#leafFn() +known_edge FUNCTION_CALL parts#() @L44 -> parts#leafNuxt() +known_edge FUNCTION_CALL parts#() @L51 -> parts#leafStore() +known_edge FUNCTION_CALL parts#render() @L35 -> parts#leafRender() +known_edge FUNCTION_CALL parts#setup(?) @L26 -> parts#leafSetup() +known_edge FUNCTION_CALL reg#() @L15 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#() @L24 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#setup() @L20 -> reg#leafLocal() +known_edge JSX_COMPONENT_CALL app#() @L15 -> card#render() +known_edge JSX_COMPONENT_CALL app#() @L16 -> parts#setup(?) +known_edge JSX_COMPONENT_CALL app#() @L17 -> parts#render() +known_edge JSX_COMPONENT_CALL app#() @L18 -> parts#(?) +known_edge JSX_COMPONENT_CALL app#() @L19 -> parts#setup()@L43 +known_edge JSX_COMPONENT_CALL app#() @L21 -> button#setup() +known_edge JSX_COMPONENT_CALL app#() @L22 -> reg#setup() +known_edge JSX_COMPONENT_CALL card#render() @L13 -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries b/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries new file mode 100644 index 000000000..e98e9f551 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.entries @@ -0,0 +1,2 @@ +── entry_point (1) ── + unimported_module app# app.tsx:1 diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle new file mode 100644 index 000000000..7aa79d058 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle @@ -0,0 +1,7 @@ +80-vue-definecomponent-jsx [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 diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges new file mode 100644 index 000000000..40aed3c84 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges @@ -0,0 +1,36 @@ +ambiguous_unknown FUNCTION_CALL parts#() @L27 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L40 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L44 -> - +ambiguous_unknown FUNCTION_CALL parts#() @L51 -> - +ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - +ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - +boundary_lib FUNCTION_CALL app#() @L10 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL button#() @L7 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL card#() @L9 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L23 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L32 -> vue#defineComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L40 -> vue#defineComponent((props: any) =) +boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent(ComponentOptions) +boundary_lib FUNCTION_CALL parts#() @L49 -> vue#makeStore(ComponentOptions) +boundary_lib FUNCTION_CALL reg#() @L18 -> vue#defineComponent(ComponentOptions) +callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() +known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() +known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() +known_edge FUNCTION_CALL parts#() @L27 -> parts#leafRendered() +known_edge FUNCTION_CALL parts#() @L40 -> parts#leafFn() +known_edge FUNCTION_CALL parts#() @L44 -> parts#leafNuxt() +known_edge FUNCTION_CALL parts#() @L51 -> parts#leafStore() +known_edge FUNCTION_CALL parts#render() @L35 -> parts#leafRender() +known_edge FUNCTION_CALL parts#setup(?) @L26 -> parts#leafSetup() +known_edge FUNCTION_CALL reg#() @L15 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#() @L24 -> reg#withInstall(T) +known_edge FUNCTION_CALL reg#setup() @L20 -> reg#leafLocal() +known_edge JSX_COMPONENT_CALL app#() @L15 -> card#render() +known_edge JSX_COMPONENT_CALL app#() @L16 -> parts#setup(?) +known_edge JSX_COMPONENT_CALL app#() @L17 -> parts#render() +known_edge JSX_COMPONENT_CALL app#() @L18 -> parts#(?) +known_edge JSX_COMPONENT_CALL app#() @L19 -> parts#setup()@L43 +known_edge JSX_COMPONENT_CALL app#() @L21 -> button#setup() +known_edge JSX_COMPONENT_CALL app#() @L22 -> reg#setup() +known_edge JSX_COMPONENT_CALL card#render() @L13 -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing new file mode 100644 index 000000000..a25fb5138 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.known-missing @@ -0,0 +1,9 @@ +# Accepted gaps for 80-vue-definecomponent-jsx — each line is an edge the TypeScript compiler +# resolves and this engine does not. A NEW missing edge fails the suite; +# a line here that STARTS working also fails, so the debt cannot rot. +# +# The compiler binds a Vue tag to the library's component CONSTRUCT signature, which says +# nothing about what renders; the engine binds it to the setup / render the options object +# defines instead. Both were missing before that rule too. +app#() -> Component#(any[]) +card#render() -> Component#(any[]) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle new file mode 100644 index 000000000..618f3a51a --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.oracle @@ -0,0 +1,11 @@ +oracle=21 engine=27 agree=19 missing=2 (known 2, NEW 0) extra=8 + known app#() -> Component#(any[]) + known card#render() -> Component#(any[]) + extra app#() -> button#setup() + extra app#() -> card#render() + extra app#() -> parts#(?) + extra app#() -> parts#render() + extra app#() -> parts#setup()@L43 + extra app#() -> parts#setup(?) + extra app#() -> reg#setup() + extra card#render() -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle new file mode 100644 index 000000000..3279e3f88 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.oracle @@ -0,0 +1,9 @@ +oracle=11 engine=19 agree=11 missing=0 (known 0, NEW 0) extra=8 + extra app#() -> button#setup() + extra app#() -> card#render() + extra app#() -> parts#(?) + extra app#() -> parts#render() + extra app#() -> parts#setup()@L43 + extra app#() -> parts#setup(?) + extra app#() -> reg#setup() + extra card#render() -> parts#setup(?) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.type-use b/graph/test/typescript/expected/80-vue-definecomponent-jsx.type-use new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle new file mode 100644 index 000000000..8be944808 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.types-oracle @@ -0,0 +1,7 @@ +80-vue-definecomponent-jsx [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 diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index b1de05bf7..1c0385665 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -653,6 +653,47 @@ jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), expr_child("client", w, "ARGUMENT", "0", a), jsx_component_wrapper(a), jsx_wrapper_renders(a, m). +// …or, when that argument NAMES a component defined elsewhere, what that one renders: the +// registration idiom `import _Button from "./Button"; export const Button = +// withInstall(_Button)`, where Button.tsx is `export default defineComponent({…})`, and +// its local and named-import forms. +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "VARIABLE", v, a), + jsx_wrapped_component(v, m). +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "IMPORT_BINDING", ih, a), + import_binds(ih, _, "VARIABLE", v), + jsx_wrapped_component(v, m). +jsx_wrapper_renders(w, m) :- jsx_component_wrapper(w), + expr_child("client", w, "ARGUMENT", "0", a), + expr_referenced("client", "IMPORT_BINDING", ih, a), + import_binding("client", "DEFAULT", _, _, _, ih), + import_target_module(ih, "client", mod2), + jsx_default_export_renders(mod2, m). +// A Vue component, `defineComponent({ setup() {…} })` / `defineComponent({ render() {…} })` +// (Nuxt: `defineNuxtComponent`). What the tag invokes with its props is the options +// object's `setup`, or its `render` option when there is no setup; a render function setup +// RETURNS is reached from setup's body, as memo's argument reaches what it defines. One +// target, not both: the site's candidates are an overload set to arity selection, and +// `setup(props)` beside a zero-parameter render arrow would lose one of them. The function +// form, `defineComponent((props) => () => …)`, is argument 0 itself, as for memo. Named by +// the definer, so the same object handed to any other call (`makeStore({ setup() {…} })`) +// stays out. +jsx_vue_definer_name("defineComponent"). +jsx_vue_definer_name("defineNuxtComponent"). +jsx_vue_definer(w) :- call_site("client", _, n, _, _, w, _), + jsx_vue_definer_name(n). +jsx_component_wrapper(w) :- jsx_vue_definer(w). +jsx_vue_has_setup(a) :- objlit_member_fn(a, "setup", _). +jsx_wrapper_renders(w, m) :- jsx_vue_definer(w), + expr_child("client", w, "ARGUMENT", "0", a), + objlit_member_fn(a, "setup", m). +jsx_wrapper_renders(w, m) :- jsx_vue_definer(w), + expr_child("client", w, "ARGUMENT", "0", a), + !jsx_vue_has_setup(a), + objlit_member_fn(a, "render", m). // A wrapper call that IS the module's default export, `export default memo(Card)`, // binds no variable: the tag reaches it through a default import of that module. jsx_default_export_renders(mod, m) :- export_decl("client", "default", _, "DEFAULT_EXPRESSION", mod, h), diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 815055c16..f8d901ec3 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -288,6 +288,9 @@ .decl jsx_wrapped_component(c0:symbol,c1:symbol) .decl jsx_wrapper_name(c0:symbol) .decl jsx_wrapper_renders(c0:symbol,c1:symbol) +.decl jsx_vue_definer_name(c0:symbol) +.decl jsx_vue_definer(c0:symbol) +.decl jsx_vue_has_setup(c0:symbol) .decl lib_boundary_reason(c0:symbol,c1:symbol) .decl lib_capped() .decl lib_export_relative_base(c0:symbol,c1:symbol)