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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
41 changes: 36 additions & 5 deletions src/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
extend,
invariant,
levenshteinDistance,
TsIdentifier,
} from "./utils/helpers";
import * as Act from "./CodeActions";
import {
Expand Down Expand Up @@ -111,14 +112,45 @@ export const OPERATION_TYPES = new Set(["Query", "Mutation", "Subscription"]);
type ArgDefaults = Map<string, ts.Expression>;

export type ExtractionSnapshot = {
/** GraphQL definitions extracted from the TypeScript source file. */
readonly definitions: DefinitionNode[];
readonly unresolvedNames: Map<ts.EntityName, NameNode>;

/**
* Map from a TypeScript AST node that may reference a GraphQL type to a
* GraphQL NameNode. Note that at extraction time we don't actually know the
* GraphQL name that this references, or if it even references a valid Grats
* type. So, the `NameNode` passed here will generally have a placeholder
* name. This will be resolved in a later pass since it may reference a type
* defined in another file and extraction is done on a per-file basis.
*/
readonly unresolvedNames: Map<TsIdentifier, ts.EntityName>;

/** Map from a TypeScript declaration to the extracted GraphQL name and kind. */
readonly nameDefinitions: Map<ts.DeclarationStatement, NameDefinition>;

/**
* Some declarations (notably derived context functions) are not actually the
* declaration that will become a special GraphQL value, but rather they
* _reference_ a type which will implicitly become a special type to Grats.
*/
readonly implicitNameDefinitions: Map<
DeclarationDefinition,
ts.TypeReferenceNode
>;

/**
* Records which named GraphQL types define a `__typename` field.
* This is used to ensure all types which are members of an abstract type
* (union or interface) define a `__typename` field which is required to
* determine their GraphQL type at runtime.
*/
readonly typesWithTypename: Set<string>;

/**
* TypeScript interfaces which have been used to define GraphQL types. This is
* used in a later validation pass to ensure we never use merged interfaces,
* since merged interfaces have surprising behaviors which can lead to bugs.
*/
readonly interfaceDeclarations: Array<ts.InterfaceDeclaration>;
};

Expand All @@ -145,10 +177,9 @@ export function extract(
}

class Extractor {
// Snapshot data. See comments on fields on ExtractionSnapshot for details.
definitions: DefinitionNode[] = [];

// Snapshot data
unresolvedNames: Map<ts.EntityName, NameNode> = new Map();
unresolvedNames: Map<TsIdentifier, ts.EntityName> = new Map();
nameDefinitions: Map<ts.DeclarationStatement, NameDefinition> = new Map();
implicitNameDefinitions: Map<DeclarationDefinition, ts.TypeReferenceNode> =
new Map();
Expand All @@ -165,7 +196,7 @@ class Extractor {
}

markUnresolvedType(node: ts.EntityName, name: NameNode) {
this.unresolvedNames.set(node, name);
this.unresolvedNames.set(name.tsIdentifier, node);
}

recordTypeName(
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQLAstExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ResolverSignature } from "./resolverSignature";
import { TsIdentifier } from "./utils/helpers";

export type ExportDefinition = {
tsModulePath: string;
Expand Down Expand Up @@ -29,7 +30,7 @@ declare module "graphql" {
* Grats metadata: A unique identifier for the node. Used to track
* data about nodes in lookup data structures.
*/
tsIdentifier: number;
tsIdentifier: TsIdentifier;
}
export interface ObjectTypeDefinitionNode {
/**
Expand Down
76 changes: 57 additions & 19 deletions src/TypeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,56 @@ export type NameDefinition = {

export type DeclarationDefinition = NameDefinition | DerivedResolverDefinition;

type TsIdentifier = number;
import type { TsIdentifier } from "./utils/helpers";

/**
* Public interface for TypeContext.
*
* Used to track TypeScript references and resolve type names between
* TypeScript and GraphQL.
*/
export interface ITypeContext {
/** Resolves an unresolved NameNode to its actual GraphQL name */
resolveUnresolvedNamedType(unresolved: NameNode): DiagnosticResult<NameNode>;

/** Checks if an unresolved NameNode refers to a GraphQL type */
unresolvedNameIsGraphQL(unresolved: NameNode): boolean;

/** Gets the declaration definition for a GraphQL NameNode */
gqlNameDefinitionForGqlName(
nameNode: NameNode,
): DiagnosticResult<DeclarationDefinition>;

/** Gets the GraphQL name for a TypeScript entity name */
gqlNameForTsName(node: ts.EntityName): DiagnosticResult<string>;
}

/**
* Additional methods implemented by TypeContext for use during type resolution.
*/
export interface ITypeContextForResolveTypes extends ITypeContext {
/**
* Gets the TypeScript declaration for a TypeScript entity name.
*/
tsDeclarationForTsName(node: ts.EntityName): DiagnosticResult<ts.Declaration>;

/**
* Gets the TypeScript declaration for a GraphQL definition node
* Currently used exclusively for taking a GraphQL declaration and
* finding its TypeScript declaration in order to find generic type
* parameters.
*/
tsDeclarationForGqlDefinition(
definition:
| ObjectTypeDefinitionNode
| UnionTypeDefinitionNode
| InputObjectTypeDefinitionNode
| InterfaceTypeDefinitionNode,
): ts.Declaration;

/** Gets the TypeScript entity name associated with a GraphQL NameNode */
getEntityName(name: NameNode): ts.EntityName | null;
}

/**
* Used to track TypeScript references.
Expand All @@ -59,23 +108,21 @@ type TsIdentifier = number;
* parsed all the files, we traverse the GraphQL schema, resolving all the dummy
* type references.
*/
export class TypeContext {
checker: ts.TypeChecker;
export class TypeContext implements ITypeContext, ITypeContextForResolveTypes {
private checker: ts.TypeChecker;

_declarationToDefinition: Map<ts.Declaration, DeclarationDefinition> =
private _declarationToDefinition: Map<ts.Declaration, DeclarationDefinition> =
new Map();
_unresolvedNodes: Map<TsIdentifier, ts.EntityName> = new Map();
_idToDeclaration: Map<TsIdentifier, ts.Declaration> = new Map();
private _unresolvedNodes: Map<TsIdentifier, ts.EntityName> = new Map();
private _idToDeclaration: Map<TsIdentifier, ts.Declaration> = new Map();

static fromSnapshot(
checker: ts.TypeChecker,
snapshot: ExtractionSnapshot,
): DiagnosticsResult<TypeContext> {
const errors: FixableDiagnosticWithLocation[] = [];
const self = new TypeContext(checker);
for (const [node, typeName] of snapshot.unresolvedNames) {
self._markUnresolvedType(node, typeName);
}
self._unresolvedNodes = snapshot.unresolvedNames;
for (const [node, definition] of snapshot.nameDefinitions) {
self._recordDeclaration(node, definition);
}
Expand Down Expand Up @@ -122,16 +169,7 @@ export class TypeContext {
this._declarationToDefinition.set(node, definition);
}

// Record that a type references `node`
private _markUnresolvedType(node: ts.EntityName, name: NameNode) {
this._unresolvedNodes.set(name.tsIdentifier, node);
}

allDefinitions(): Iterable<DeclarationDefinition> {
return this._declarationToDefinition.values();
}

findSymbolDeclaration(startSymbol: ts.Symbol): ts.Declaration | null {
private findSymbolDeclaration(startSymbol: ts.Symbol): ts.Declaration | null {
const symbol = this.resolveSymbol(startSymbol);
const declaration = symbol.declarations?.[0];
return declaration ?? null;
Expand Down
4 changes: 2 additions & 2 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function extractSchemaAndDoc(
// Collect validation errors
const validationResult = concatResults(
validateMergedInterfaces(checker, snapshot.interfaceDeclarations),
validateDuplicateContextOrInfo(ctx),
validateDuplicateContextOrInfo(snapshot.nameDefinitions.values()),
);

const docResult = new ResultPipe(validationResult)
Expand All @@ -120,7 +120,7 @@ export function extractSchemaAndDoc(
.andThen((definitions) => resolveTypes(ctx, definitions))
// Convert string literals used as default values for enums into GraphQL
// enums where appropriate.
.map((definitions) => coerceDefaultEnumValues(ctx, definitions))
.map((definitions) => coerceDefaultEnumValues(definitions))
// If you define a field on an interface using the functional style, we
// need to add that field to each concrete type as well. This must be
// done after all types are created, but before we validate the schema.
Expand Down
3 changes: 2 additions & 1 deletion src/tests/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export default class TestRunner {
const fileType = path.extname(fixture).slice(1);

const output = new Markdown();
output.addHeader(2, "input");
output.addHeader(1, fixture);
output.addHeader(2, "Input");
output.addCodeBlock(fixtureContent, fileType, fixture);
output.addHeader(2, "Output");
if (actualOutput instanceof Markdown) {
Expand Down
4 changes: 3 additions & 1 deletion src/tests/configParserFixtures/empty.json.expected.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# empty.json

## Input

```json title="empty.json"
{}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# experimentalField.json

## Input

```json title="experimentalField.json"
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# invlaidKey.invalid.json

## Input

```json title="invlaidKey.invalid.json"
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# multiLineHeader.json

## Input

```json title="multiLineHeader.json"
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# multiLineNonHeader.invalid.json

## Input

```json title="multiLineNonHeader.invalid.json"
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# nonNullableFieldIsNull.invalid.json

## Input

```json title="nonNullableFieldIsNull.invalid.json"
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/ArgReferencesNonGqlType.invalid.ts

## Input

```ts title="arguments/ArgReferencesNonGqlType.invalid.ts"
type NotGraphql = any;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/ArgWithNoType.invalid.ts

## Input

```ts title="arguments/ArgWithNoType.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/ArgumentWithDescription.ts

## Input

```ts title="arguments/ArgumentWithDescription.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/AsyncIterableArgument.invalid.ts

## Input

```ts title="arguments/AsyncIterableArgument.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/CustomScalarArgument.ts

## Input

```ts title="arguments/CustomScalarArgument.ts"
/** @gqlScalar */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/DeprecatedArgument.ts

## Input

```ts title="arguments/DeprecatedArgument.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/DeprecatedRequiredArgument.invalid.ts

## Input

```ts title="arguments/DeprecatedRequiredArgument.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/GqlTypeUsedAsPositionalArg.ts

## Input

```ts title="arguments/GqlTypeUsedAsPositionalArg.ts"
/** @gqlInput */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/MultipleParamsTypedAsTypeLiteral.invalid.ts

## Input

```ts title="arguments/MultipleParamsTypedAsTypeLiteral.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/NoArgsWithNever.invalid.ts

## Input

```ts title="arguments/NoArgsWithNever.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/NoArgsWithUnknown.invalid.ts

## Input

```ts title="arguments/NoArgsWithUnknown.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/NoTypeAnnotation.invalid.ts

## Input

```ts title="arguments/NoTypeAnnotation.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/NullableArgumentErrors.invalid.ts

## Input

```ts title="arguments/NullableArgumentErrors.invalid.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/NullableArguments.ts

## Input

```ts title="arguments/NullableArguments.ts"
/** @gqlType */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## input
# arguments/ObjectLiteralArgument.invalid.ts

## Input

```ts title="arguments/ObjectLiteralArgument.invalid.ts"
/** @gqlType */
Expand Down
Loading