Skip to content

Commit

Permalink
fix: support for generic functions (#2159)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominik Moritz <[email protected]>
  • Loading branch information
arthurfiorette and domoritz authored Feb 18, 2025
1 parent 6ad76cf commit c8ea3ed
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 13 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"vega": "^5.31.0",
"vega-lite": "^5.23.0"
},
"packageManager": "[email protected]",
"engines": {
"node": ">=18.0.0"
}
Expand Down
6 changes: 6 additions & 0 deletions src/ChainNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class ChainNodeParser implements SubNodeParser, MutableParser {
}
const contextCacheKey = context.getCacheKey();
let type = typeCache.get(contextCacheKey);

if (!type) {
try {
type = this.getNodeParser(node).createType(node, context, reference);
Expand All @@ -41,6 +42,11 @@ export class ChainNodeParser implements SubNodeParser, MutableParser {
typeCache.set(contextCacheKey, type);
}
}

if (!type) {
throw new UnknownNodeError(node);
}

return type;
}

Expand Down
19 changes: 16 additions & 3 deletions src/NodeParser/CallExpressionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { BaseType } from "../Type/BaseType.js";
import { UnionType } from "../Type/UnionType.js";
import { LiteralType } from "../Type/LiteralType.js";
import { SymbolType } from "../Type/SymbolType.js";
import { UnknownNodeError } from "../Error/Errors.js";

export class CallExpressionParser implements SubNodeParser {
public constructor(
Expand All @@ -33,9 +34,21 @@ export class CallExpressionParser implements SubNodeParser {
}

const symbol = type.symbol || type.aliasSymbol;
const decl = symbol.valueDeclaration || symbol.declarations![0];
const subContext = this.createSubContext(node, context);
return this.childNodeParser.createType(decl, subContext);

// For funtions like <T>(type: T) => T, there won't be any reference to the original
// type. Using type checker to infer the actual return type without mapping the whole
// function and back referencing its generic type based on parameter index is a better
// approach.
const decl =
this.typeChecker.typeToTypeNode(type, node, ts.NodeBuilderFlags.IgnoreErrors) ||
symbol.valueDeclaration ||
symbol.declarations?.[0];

if (!decl) {
throw new UnknownNodeError(node);
}

return this.childNodeParser.createType(decl, this.createSubContext(node, context));
}

protected createSubContext(node: ts.CallExpression, parentContext: Context): Context {
Expand Down
5 changes: 3 additions & 2 deletions src/NodeParser/IndexedAccessTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ts from "typescript";
import { LogicError } from "../Error/Errors.js";
import type { Context, NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import type { BaseType } from "../Type/BaseType.js";
Expand All @@ -9,9 +10,9 @@ import { ReferenceType } from "../Type/ReferenceType.js";
import { StringType } from "../Type/StringType.js";
import { TupleType } from "../Type/TupleType.js";
import { UnionType } from "../Type/UnionType.js";
import { isErroredUnknownType } from "../Type/UnknownType.js";
import { derefType } from "../Utils/derefType.js";
import { getTypeByKey } from "../Utils/typeKeys.js";
import { LogicError } from "../Error/Errors.js";

export class IndexedAccessTypeNodeParser implements SubNodeParser {
public constructor(
Expand Down Expand Up @@ -49,7 +50,7 @@ export class IndexedAccessTypeNodeParser implements SubNodeParser {
const indexType = derefType(this.childNodeParser.createType(node.indexType, context));
const indexedType = this.createIndexedType(node.objectType, context, indexType);

if (indexedType) {
if (indexedType && !isErroredUnknownType(indexedType)) {
return indexedType;
}

Expand Down
3 changes: 2 additions & 1 deletion src/NodeParser/TypeReferenceNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AnyType } from "../Type/AnyType.js";
import { ArrayType } from "../Type/ArrayType.js";
import type { BaseType } from "../Type/BaseType.js";
import { StringType } from "../Type/StringType.js";
import { UnknownType } from "../Type/UnknownType.js";
import { symbolAtNode } from "../Utils/symbolAtNode.js";

const invalidTypes: Record<number, boolean> = {
Expand Down Expand Up @@ -45,7 +46,7 @@ export class TypeReferenceNodeParser implements SubNodeParser {
}

if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) {
return context.getArgument(typeSymbol.name);
return context.getArgument(typeSymbol.name) ?? new UnknownType(true);
}

// Wraps promise type to avoid resolving to a empty Object type.
Expand Down
6 changes: 3 additions & 3 deletions src/NodeParser/UnknownTypeNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ts from "typescript";
import type { Context } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import type { BaseType } from "../Type/BaseType.js";
import { UnknownType } from "../Type/UnknownType.js";
Expand All @@ -8,7 +7,8 @@ export class UnknownTypeNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.UnknownKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
return new UnknownType();

public createType(): BaseType {
return new UnknownType(false);
}
}
15 changes: 14 additions & 1 deletion src/Type/UnknownType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { BaseType } from "./BaseType.js";

export class UnknownType extends BaseType {
constructor() {
constructor(
/**
* If the source for this UnknownType was from a failed operation than to an actual `unknown` type present in the source code.
*/
readonly erroredSource: boolean,
) {
super();
}

public getId(): string {
return "unknown";
}
}

/**
* Checks for an UnknownType with an errored source.
*/
export function isErroredUnknownType(type: BaseType): type is UnknownType {
return type instanceof UnknownType && type.erroredSource;
}
8 changes: 7 additions & 1 deletion src/TypeFormatter/UnknownTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ export class UnknownTypeFormatter implements SubTypeFormatter {
public supportsType(type: BaseType): boolean {
return type instanceof UnknownType;
}

public getDefinition(type: UnknownType): Definition {
if (type.erroredSource) {
return { description: "Failed to correctly infer type" };
}

return {};
}
public getChildren(type: UnknownType): BaseType[] {

public getChildren(): BaseType[] {
return [];
}
}
1 change: 1 addition & 0 deletions test/valid-data-other.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe("valid-data-other", () => {
it("array-function-generics", assertValidSchema("array-function-generics", "*"));
it("array-max-items-optional", assertValidSchema("array-max-items-optional", "MyType"));
it("shorthand-array", assertValidSchema("shorthand-array", "MyType"));
it("function-generic", assertValidSchema("function-generic", "MyType"));

it(
"object-required",
Expand Down
8 changes: 6 additions & 2 deletions test/valid-data/array-function-generics/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
"additionalProperties": false,
"properties": {
"a": {
"items": {},
"items": {
"description": "Failed to correctly infer type"
},
"type": "array"
},
"b": {
"items": {},
"items": {
"description": "Failed to correctly infer type"
},
"type": "array"
}
},
Expand Down
12 changes: 12 additions & 0 deletions test/valid-data/function-generic/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function fn<A, B, C>(a: A, b: B, c: C, any: A | B | C) {
return { a, b, c, any };
}

const value = {
litNum: fn(1, "2", true, 1),
litStr: fn("1", 2, true, "2"),
litBool: fn(true, 2, "3", true),
obj: fn({ a: 1 }, { b: "2" }, { c: true }, { a: 1 }),
};

export type MyType = typeof value;
190 changes: 190 additions & 0 deletions test/valid-data/function-generic/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
{
"$ref": "#/definitions/MyType",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyType": {
"additionalProperties": false,
"properties": {
"litBool": {
"additionalProperties": false,
"properties": {
"a": {
"type": "boolean"
},
"any": {
"type": [
"string",
"number",
"boolean"
]
},
"b": {
"type": "number"
},
"c": {
"type": "string"
}
},
"required": [
"a",
"b",
"c",
"any"
],
"type": "object"
},
"litNum": {
"additionalProperties": false,
"properties": {
"a": {
"type": "number"
},
"any": {
"type": [
"string",
"number",
"boolean"
]
},
"b": {
"type": "string"
},
"c": {
"type": "boolean"
}
},
"required": [
"a",
"b",
"c",
"any"
],
"type": "object"
},
"litStr": {
"additionalProperties": false,
"properties": {
"a": {
"type": "string"
},
"any": {
"type": [
"string",
"number",
"boolean"
]
},
"b": {
"type": "number"
},
"c": {
"type": "boolean"
}
},
"required": [
"a",
"b",
"c",
"any"
],
"type": "object"
},
"obj": {
"additionalProperties": false,
"properties": {
"a": {
"additionalProperties": false,
"properties": {
"a": {
"type": "number"
}
},
"required": [
"a"
],
"type": "object"
},
"any": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"a": {
"type": "number"
}
},
"required": [
"a"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"b": {
"type": "string"
}
},
"required": [
"b"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"c": {
"type": "boolean"
}
},
"required": [
"c"
],
"type": "object"
}
]
},
"b": {
"additionalProperties": false,
"properties": {
"b": {
"type": "string"
}
},
"required": [
"b"
],
"type": "object"
},
"c": {
"additionalProperties": false,
"properties": {
"c": {
"type": "boolean"
}
},
"required": [
"c"
],
"type": "object"
}
},
"required": [
"a",
"b",
"c",
"any"
],
"type": "object"
}
},
"required": [
"litNum",
"litStr",
"litBool",
"obj"
],
"type": "object"
}
}
}
Loading

0 comments on commit c8ea3ed

Please sign in to comment.