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: targets returning void should type check #336

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"useConst": "off"
},
"suspicious": {
"noConfusingVoidType": "off",
"noExplicitAny": "off",
"noRedeclare": "off",
"useIsArray": "off"
Expand Down
40 changes: 22 additions & 18 deletions packages/runtime/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import * as im from "immutable";
import im from "immutable";
import * as tg from "./index.ts";
import type { MaybePromise } from "./util.ts";

export type Unresolved<T extends tg.Value> = MaybePromise<
T extends
| undefined
| boolean
| number
| string
| tg.Object
| Uint8Array
| tg.Mutation
| tg.Template
? T
: T extends Array<infer U extends tg.Value>
? Array<Unresolved<U>>
: T extends { [key: string]: tg.Value }
? { [K in keyof T]: Unresolved<T[K]> }
: never
>;
export type Unresolved<T extends tg.Value> = MaybePromise<UnresolvedInner<T>>;

export type UnresolvedInner<T extends tg.Value> = T extends
| undefined
| boolean
| number
| string
| tg.Object
| Uint8Array
| tg.Mutation
| tg.Template
? T
: T extends Array<infer U extends tg.Value>
? Array<Unresolved<U>>
: T extends {
[key: string]: tg.Value;
}
? {
[K in keyof T]: Unresolved<T[K]>;
}
: never;

export type Resolved<T extends Unresolved<tg.Value>> = T extends
| undefined
Expand Down
19 changes: 11 additions & 8 deletions packages/runtime/src/target.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as tg from "./index.ts";
import type { UnresolvedInner } from "./resolve.ts";
import {
type MaybeMutationMap,
type MaybeNestedArray,
Expand All @@ -14,24 +15,26 @@ export let setCurrentTarget = (target: Target) => {

type FunctionArg<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
> = {
function: (...args: A) => tg.Unresolved<R>;
function: (
...args: A
) => MaybePromise<R extends void ? void : UnresolvedInner<Exclude<R, void>>>;
module: tg.Module;
name: string;
};

export function target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
>(arg: FunctionArg): Target<A, R>;
export function target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
>(...args: tg.Args<Target.Arg>): Promise<Target<A, R>>;
export function target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
>(
...args: [FunctionArg<A, R>] | tg.Args<Target.Arg>
): MaybePromise<Target<A, R>> {
Expand Down Expand Up @@ -70,15 +73,15 @@ export function target<

export interface Target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
> extends globalThis.Function {
(...args: { [K in keyof A]: tg.Unresolved<A[K]> }): Promise<R>;
}

// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: This is necessary to make targets callable.
export class Target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
> extends globalThis.Function {
#state: Target.State;
#f: Function | undefined;
Expand Down Expand Up @@ -116,7 +119,7 @@ export class Target<

static async new<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
>(...args: tg.Args<Target.Arg>): Promise<Target<A, R>> {
let arg = await Target.arg(...args);
let args_ = arg.args ?? [];
Expand Down
56 changes: 35 additions & 21 deletions packages/runtime/tangram.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ declare namespace tg {
}

/** Create a symlink. */
export let symlink: (arg: tg.Unresolved<tg.Symlink.Arg>) => Promise<tg.Symlink>;
export let symlink: (
arg: tg.Unresolved<tg.Symlink.Arg>,
) => Promise<tg.Symlink>;

/** A symlink. */
export class Symlink {
Expand Down Expand Up @@ -546,17 +548,23 @@ declare namespace tg {
/** Create a target. */
export function target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
>(function_: (...args: A) => tg.Unresolved<R>): tg.Target<A, R>;
R extends void | tg.Value = void | tg.Value,
>(
function_: (
...args: A
) => MaybePromise<
R extends void ? void : tg.UnresolvedInner<Exclude<R, void>>
>,
): tg.Target<A, R>;
export function target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
>(...args: tg.Args<tg.Target.Arg>): Promise<tg.Target<A, R>>;

/** A target. */
export interface Target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
> {
/** Build this target. */
// biome-ignore lint/style/useShorthandFunctionType: interface is necessary .
Expand All @@ -566,7 +574,7 @@ declare namespace tg {
/** A target. */
export class Target<
A extends Array<tg.Value> = Array<tg.Value>,
R extends tg.Value = tg.Value,
R extends void | tg.Value = void | tg.Value,
> extends globalThis.Function {
/** Get a target with an ID. */
static withId(id: tg.Target.Id): tg.Target;
Expand Down Expand Up @@ -941,23 +949,29 @@ declare namespace tg {
* ```
*/
export type Unresolved<T extends tg.Value> = tg.MaybePromise<
T extends
| undefined
| boolean
| number
| string
| tg.Object
| Uint8Array
| tg.Mutation
| tg.Template
? T
: T extends Array<infer U extends tg.Value>
? Array<tg.Unresolved<U>>
: T extends { [key: string]: tg.Value }
? { [K in keyof T]: tg.Unresolved<T[K]> }
: never
tg.UnresolvedInner<T>
>;

type UnresolvedInner<T extends tg.Value> = T extends
| undefined
| boolean
| number
| string
| tg.Object
| Uint8Array
| tg.Mutation
| tg.Template
? T
: T extends Array<infer U extends tg.Value>
? Array<Unresolved<U>>
: T extends {
[key: string]: tg.Value;
}
? {
[K in keyof T]: Unresolved<T[K]>;
}
: never;

/**
* This computed type performs the inverse of `Unresolved`. It takes a type and returns the output of calling `resolve` on a value of that type. Here are some examples:
*
Expand Down
20 changes: 20 additions & 0 deletions packages/server/src/package/check/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ async fn nonexistent_function() -> tg::Result<()> {
.await
}

#[tokio::test]
async fn no_return_value() -> tg::Result<()> {
test(
temp::directory! {
"tangram.ts" => indoc!(r"
export default tg.target(() => {});
"),
},
|_, output| async move {
assert_json_snapshot!(output, @r#"
{
"diagnostics": []
}
"#);
Ok(())
},
)
.await
}

async fn test<F, Fut>(artifact: temp::Artifact, assertions: F) -> tg::Result<()>
where
F: FnOnce(Server, tg::package::check::Output) -> Fut,
Expand Down