Skip to content

Support "duplicated" type definitions #1874

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

Closed
wants to merge 3 commits into from
Closed
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 index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from "./src/Utils/isAssignableTo";
export * from "./src/Utils/isHidden";
export * from "./src/Utils/modifiers";
export * from "./src/Utils/narrowType";
export * from "./src/Utils/nodeFilename";
export * from "./src/Utils/nodeKey";
export * from "./src/Utils/notNever";
export * from "./src/Utils/preserveAnnotation";
Expand Down
4 changes: 3 additions & 1 deletion src/NodeParser/EnumNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BaseType } from "../Type/BaseType";
import { EnumType, EnumValue } from "../Type/EnumType";
import { isNodeHidden } from "../Utils/isHidden";
import { getKey } from "../Utils/nodeKey";
import { nodeFilename } from "../Utils/nodeFilename";

export class EnumNodeParser implements SubNodeParser {
public constructor(protected typeChecker: ts.TypeChecker) {}
Expand All @@ -19,7 +20,8 @@ export class EnumNodeParser implements SubNodeParser {
`enum-${getKey(node, context)}`,
members
.filter((member: ts.EnumMember) => !isNodeHidden(member))
.map((member, index) => this.getMemberValue(member, index))
.map((member, index) => this.getMemberValue(member, index)),
nodeFilename(node)
);
}

Expand Down
5 changes: 4 additions & 1 deletion src/NodeParser/FunctionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SubNodeParser } from "../SubNodeParser";
import { ObjectProperty, ObjectType } from "../Type/ObjectType";
import { getKey } from "../Utils/nodeKey";
import { DefinitionType } from "../Type/DefinitionType";
import { nodeFilename } from "../Utils/nodeFilename";

/**
* This function parser supports both `FunctionDeclaration` & `ArrowFunction` nodes.
Expand Down Expand Up @@ -39,7 +40,9 @@ export class FunctionParser implements SubNodeParser {

return new ObjectProperty(node.parameters[index].name.getText(), parameterType, required);
}),
false
false,
false,
nodeFilename(node)
);
return new DefinitionType(this.getTypeName(node, context), namedArguments);
}
Expand Down
10 changes: 9 additions & 1 deletion src/NodeParser/InterfaceAndClassNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ReferenceType } from "../Type/ReferenceType";
import { isNodeHidden } from "../Utils/isHidden";
import { isPublic, isStatic } from "../Utils/modifiers";
import { getKey } from "../Utils/nodeKey";
import { nodeFilename } from "../Utils/nodeFilename";

export class InterfaceAndClassNodeParser implements SubNodeParser {
public constructor(
Expand Down Expand Up @@ -60,7 +61,14 @@ export class InterfaceAndClassNodeParser implements SubNodeParser {
}
}

return new ObjectType(id, this.getBaseTypes(node, context), properties, additionalProperties);
return new ObjectType(
id,
this.getBaseTypes(node, context),
properties,
additionalProperties,
false,
nodeFilename(node)
);
}

/**
Expand Down
21 changes: 16 additions & 5 deletions src/NodeParser/MappedTypeNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { derefAnnotatedType, derefType } from "../Utils/derefType";
import { getKey } from "../Utils/nodeKey";
import { preserveAnnotation } from "../Utils/preserveAnnotation";
import { removeUndefined } from "../Utils/removeUndefined";
import { nodeFilename } from "../Utils/nodeFilename";

export class MappedTypeNodeParser implements SubNodeParser {
public constructor(
Expand All @@ -33,18 +34,28 @@ export class MappedTypeNodeParser implements SubNodeParser {
const constraintType = this.childNodeParser.createType(node.typeParameter.constraint!, context);
const keyListType = derefType(constraintType);
const id = `indexed-type-${getKey(node, context)}`;
const srcFileName = nodeFilename(node);

if (keyListType instanceof UnionType) {
// Key type resolves to a set of known properties
return new ObjectType(
id,
[],
this.getProperties(node, keyListType, context),
this.getAdditionalProperties(node, keyListType, context)
this.getAdditionalProperties(node, keyListType, context),
false,
srcFileName
);
} else if (keyListType instanceof LiteralType) {
// Key type resolves to single known property
return new ObjectType(id, [], this.getProperties(node, new UnionType([keyListType]), context), false);
return new ObjectType(
id,
[],
this.getProperties(node, new UnionType([keyListType]), context),
false,
false,
srcFileName
);
} else if (
keyListType instanceof StringType ||
keyListType instanceof NumberType ||
Expand All @@ -60,7 +71,7 @@ export class MappedTypeNodeParser implements SubNodeParser {
// Key type widens to `string`
const type = this.childNodeParser.createType(node.type!, context);
// const resultType = type instanceof NeverType ? new NeverType() : new ObjectType(id, [], [], type);
const resultType = new ObjectType(id, [], [], type);
const resultType = new ObjectType(id, [], [], type, false, srcFileName);
if (resultType) {
let annotations;

Expand All @@ -78,9 +89,9 @@ export class MappedTypeNodeParser implements SubNodeParser {
}
return resultType;
} else if (keyListType instanceof EnumType) {
return new ObjectType(id, [], this.getValues(node, keyListType, context), false);
return new ObjectType(id, [], this.getValues(node, keyListType, context), false, false, srcFileName);
} else if (keyListType instanceof NeverType) {
return new ObjectType(id, [], [], false);
return new ObjectType(id, [], [], false, false, srcFileName);
} else {
throw new LogicError(
// eslint-disable-next-line max-len
Expand Down
3 changes: 2 additions & 1 deletion src/NodeParser/ObjectLiteralExpressionNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { getKey } from "../Utils/nodeKey";
import { ObjectProperty, ObjectType } from "../Type/ObjectType";
import { nodeFilename } from "../Utils/nodeFilename";

export class ObjectLiteralExpressionNodeParser implements SubNodeParser {
public constructor(protected childNodeParser: NodeParser) {}
Expand All @@ -23,6 +24,6 @@ export class ObjectLiteralExpressionNodeParser implements SubNodeParser {
)
);

return new ObjectType(`object-${getKey(node, context)}`, [], properties, false);
return new ObjectType(`object-${getKey(node, context)}`, [], properties, false, false, nodeFilename(node));
}
}
3 changes: 2 additions & 1 deletion src/NodeParser/ObjectTypeNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { ObjectType } from "../Type/ObjectType";
import { getKey } from "../Utils/nodeKey";
import { nodeFilename } from "../Utils/nodeFilename";

export class ObjectTypeNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.ObjectKeyword;
}

public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
return new ObjectType(`object-${getKey(node, context)}`, [], [], true, true);
return new ObjectType(`object-${getKey(node, context)}`, [], [], true, true, nodeFilename(node));
}
}
3 changes: 2 additions & 1 deletion src/NodeParser/TypeAliasNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BaseType } from "../Type/BaseType";
import { NeverType } from "../Type/NeverType";
import { ReferenceType } from "../Type/ReferenceType";
import { getKey } from "../Utils/nodeKey";
import { nodeFilename } from "../Utils/nodeFilename";

export class TypeAliasNodeParser implements SubNodeParser {
public constructor(
Expand Down Expand Up @@ -41,7 +42,7 @@ export class TypeAliasNodeParser implements SubNodeParser {
if (type instanceof NeverType) {
return new NeverType();
}
return new AliasType(id, type);
return new AliasType(id, type, nodeFilename(node));
}

protected getTypeId(node: ts.TypeAliasDeclaration, context: Context): string {
Expand Down
10 changes: 9 additions & 1 deletion src/NodeParser/TypeLiteralNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ObjectProperty, ObjectType } from "../Type/ObjectType";
import { ReferenceType } from "../Type/ReferenceType";
import { isNodeHidden } from "../Utils/isHidden";
import { getKey } from "../Utils/nodeKey";
import { nodeFilename } from "../Utils/nodeFilename";

export class TypeLiteralNodeParser implements SubNodeParser {
public constructor(
Expand All @@ -32,7 +33,14 @@ export class TypeLiteralNodeParser implements SubNodeParser {
return new NeverType();
}

return new ObjectType(id, [], properties, this.getAdditionalProperties(node, context));
return new ObjectType(
id,
[],
properties,
this.getAdditionalProperties(node, context),
false,
nodeFilename(node)
);
}

protected getProperties(node: ts.TypeLiteralNode, context: Context): ObjectProperty[] | undefined {
Expand Down
3 changes: 2 additions & 1 deletion src/NodeParser/TypeofNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReferenceType } from "../Type/ReferenceType";
import { getKey } from "../Utils/nodeKey";
import { LiteralType } from "../Type/LiteralType";
import { UnknownType } from "../Type/UnknownType";
import { nodeFilename } from "../Utils/nodeFilename";

export class TypeofNodeParser implements SubNodeParser {
public constructor(
Expand Down Expand Up @@ -78,6 +79,6 @@ export class TypeofNodeParser implements SubNodeParser {
return new ObjectProperty(name, type, true);
});

return new ObjectType(id, [], properties, false);
return new ObjectType(id, [], properties, false, false, nodeFilename(node));
}
}
52 changes: 36 additions & 16 deletions src/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { localSymbolAtNode, symbolAtNode } from "./Utils/symbolAtNode";
import { removeUnreachable } from "./Utils/removeUnreachable";
import { Config } from "./Config";
import { hasJsDocTag } from "./Utils/hasJsDocTag";
import { unambiguousName } from "./Utils/unambiguousName";
import { resolveIdRefs } from "./Utils/resolveIdRefs";
import { JSONSchema7 } from "json-schema";

export class SchemaGenerator {
public constructor(
Expand All @@ -31,17 +34,26 @@ export class SchemaGenerator {
});

const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined;

const definitions: StringMap<Definition> = {};
rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions));
// Definitions will be referred to by their ID.
// This "ID → name" map will be used to resolve object
// names before delivering the final schema to caller.
const idToNameMap = new Map<string, string>();

rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions, idToNameMap));

const reachableDefinitions = removeUnreachable(rootTypeDefinition, definitions);

return {
const schema = {
...(this.config?.schemaId ? { $id: this.config.schemaId } : {}),
$schema: "http://json-schema.org/draft-07/schema#",
...(rootTypeDefinition ?? {}),
definitions: reachableDefinitions,
};

// Finally, replace all IDs by their actual names.
return resolveIdRefs(schema, idToNameMap, this.config?.encodeRefs ?? true) as JSONSchema7;
}

protected getRootNodes(fullName: string | undefined): ts.Node[] {
Expand Down Expand Up @@ -79,7 +91,11 @@ export class SchemaGenerator {
protected getRootTypeDefinition(rootType: BaseType): Definition {
return this.typeFormatter.getDefinition(rootType);
}
protected appendRootChildDefinitions(rootType: BaseType, childDefinitions: StringMap<Definition>): void {
protected appendRootChildDefinitions(
rootType: BaseType,
childDefinitions: StringMap<Definition>,
idToNameMap: Map<string, string>
): void {
const seen = new Set<string>();

const children = this.typeFormatter
Expand All @@ -93,25 +109,29 @@ export class SchemaGenerator {
return false;
});

const ids = new Map<string, string>();
// identify duplicated definitions. ie: definitions with distinct
// origins but sharing the same name
const duplicates: StringMap<Set<DefinitionType>> = {};
for (const child of children) {
const name = child.getName();
const previousId = ids.get(name);
// remove def prefix from ids to avoid false alarms
// FIXME: we probably shouldn't be doing this as there is probably something wrong with the deduplication
const childId = child.getId().replace(/def-/g, "");

if (previousId && childId !== previousId) {
throw new Error(`Type "${name}" has multiple definitions.`);
}
ids.set(name, childId);
duplicates[name] ??= new Set<DefinitionType>();
duplicates[name].add(child);
}

// for duplicated definitions, lift the name ambiguity by renaming them.
children.reduce((definitions, child) => {
const name = child.getName();
if (!(name in definitions)) {
definitions[name] = this.typeFormatter.getDefinition(child.getType());
const id = child.getId();
if (!(id in definitions)) {
const name = unambiguousName(child, child === rootType, [...duplicates[child.getName()]]);

// associate the schema to its ID, allowing steps like removeUnreachable to work
definitions[id] = this.typeFormatter.getDefinition(child.getType());

// this ID → name map will be used in the final step, to resolve object
// names before delivering the final schema to caller
idToNameMap.set(id, name);
}

return definitions;
}, childDefinitions);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Type/AliasType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { BaseType } from "./BaseType";
export class AliasType extends BaseType {
public constructor(
private id: string,
private type: BaseType
private type: BaseType,
srcFileName?: string
) {
super();
super(srcFileName);
}

public getId(): string {
Expand Down
4 changes: 4 additions & 0 deletions src/Type/AnnotatedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ export class AnnotatedType extends BaseType {
public isNullable(): boolean {
return this.nullable;
}

public getSrcFileName(): string | null {
return this.type.getSrcFileName();
}
}
14 changes: 14 additions & 0 deletions src/Type/BaseType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
export abstract class BaseType {
private srcFileName: string | null;

public abstract getId(): string;

public constructor(srcFileName?: string) {
this.srcFileName = srcFileName || null;
}

/**
* Get the definition name of the type. Override for non-basic types.
*/
public getName(): string {
return this.getId();
}

/**
* Name of the file in which the type is defined.
* Only expected to be valued for the following types: Alias, Enum, Class, Interface.
*/
public getSrcFileName(): string | null {
return this.srcFileName;
}
}
6 changes: 5 additions & 1 deletion src/Type/DefinitionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class DefinitionType extends BaseType {
}

public getId(): string {
return `def-${this.type.getId()}`;
return this.type.getId();
}

public getName(): string {
Expand All @@ -19,4 +19,8 @@ export class DefinitionType extends BaseType {
public getType(): BaseType {
return this.type;
}

public getSrcFileName(): string | null {
return this.type.getSrcFileName();
}
}
5 changes: 3 additions & 2 deletions src/Type/EnumType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export class EnumType extends BaseType {

public constructor(
private id: string,
private values: readonly EnumValue[]
private values: readonly EnumValue[],
srcFileName?: string
) {
super();
super(srcFileName);
this.types = values.map((value) => (value == null ? new NullType() : new LiteralType(value)));
}

Expand Down
Loading