Skip to content
Open
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
24 changes: 18 additions & 6 deletions llm-docs/getting-started/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,30 +55,42 @@ Options:
-h, --help display help for command

Commands:
locate [options] <ENTITY>
locate [options] <COORDINATE>
```

## Locate

The `locate` command reports the location (file, line, column) at which a given type or field is defined in your code. `grats locate` can also be invoked by other tools. For example the click-to-definition feature of an GraphQL editor integration could use invoke this command to find the location of a type or field.
The `locate` command reports the location (file, line, column) at which a given schema element is defined in your code. It accepts a [Schema Coordinate](https://spec.graphql.org/draft/#sec-Schema-Coordinates) as its argument.

`grats locate` can also be invoked by other tools. For example the click-to-definition feature of a GraphQL editor integration could invoke this command to find the location of a type or field.

For example, Relay's VSCode Extension is [exploring](https://github.com/facebook/relay/pull/4434) adding the ability to leverage such a tool.

```bash
# Locate a named type
npx grats locate User

# Locate a field
npx grats locate User.name

# Locate a named type
npx grats locate User
# Locate a field argument
npx grats locate "Query.user(id:)"

# Locate an enum value
npx grats locate "MyEnum.VALUE"

# Locate a directive
npx grats locate @deprecated
```

### Options

```text
Usage: grats locate [options] <ENTITY>
Usage: grats locate [options] <COORDINATE>

Arguments:
ENTITY GraphQL entity to locate. E.g. `User` or `User.id`
COORDINATE Schema coordinate to locate. E.g. `User`, `User.name`,
`Query.user(id:)`, `@deprecated`

Options:
--tsconfig <TSCONFIG> Path to tsconfig.json. Defaults to auto-detecting based on the current working directory
Expand Down
154 changes: 96 additions & 58 deletions src/Locate.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,115 @@
import {
GraphQLSchema,
Location,
isObjectType,
isInterfaceType,
isInputObjectType,
resolveSchemaCoordinate,
type ResolvedSchemaElement,
} from "graphql";
import { Result, err, ok } from "./utils/Result.js";
import { nullThrows } from "./utils/helpers.js";

type EntityName = {
parent: string;
field: string | null;
};

/**
* Given an entity name of the format `ParentType` or `ParentType.fieldName`,
* locate the entity in the schema and return its location.
* Given a schema coordinate string, locate the entity in the schema
* and return its source location.
*
* Uses the Schema Coordinates spec:
* https://spec.graphql.org/draft/#sec-Schema-Coordinates
*
* Supports all schema coordinate forms:
* - `Type` — named type
* - `Type.field` — field on object/interface type
* - `Type.field(arg:)` — field argument
* - `EnumType.VALUE` — enum value
* - `InputType.field` — input field
* - `@directive` — directive
* - `@directive(arg:)` — directive argument
*/
export function locate(
schema: GraphQLSchema,
entityName: string,
coordinate: string,
): Result<Location, string> {
const entityResult = parseEntityName(entityName);
if (entityResult.kind === "ERROR") {
return entityResult;
}
const entity = entityResult.value;
const type = schema.getType(entity.parent);
if (type == null) {
return err(`Cannot locate type \`${entity.parent}\`.`);
}
if (entity.field == null) {
if (type.astNode == null) {
throw new Error(
`Grats bug: Cannot find location of type \`${entity.parent}\`.`,
);
}
return ok(nullThrows(type.astNode.name.loc));
}

if (
!(isObjectType(type) || isInterfaceType(type) || isInputObjectType(type))
) {
let resolved: ResolvedSchemaElement | undefined;
try {
resolved = resolveSchemaCoordinate(schema, coordinate);
} catch (e: unknown) {
return err(
`Cannot locate field \`${entity.field}\` on type \`${entity.parent}\`. Only object types, interfaces, and input objects have fields.`,
`Invalid schema coordinate: \`${coordinate}\`. ${e instanceof Error ? e.message : String(e)}`,
);
}

const field = type.getFields()[entity.field];
if (field == null) {
return err(
`Cannot locate field \`${entity.field}\` on type \`${entity.parent}\`.`,
);
}

if (field.astNode == null) {
throw new Error(
`Grats bug: Cannot find location of field \`${entity.field}\` on type \`${entity.parent}\`.`,
);
if (resolved == null) {
return err(`Could not resolve schema coordinate: \`${coordinate}\`.`);
}
return ok(nullThrows(field.astNode.name.loc));
}

const ENTITY_NAME_REGEX = /^([A-Za-z0-9_]+)(?:\.([A-Za-z0-9_]+))?$/;

function parseEntityName(entityName: string): Result<EntityName, string> {
const match = ENTITY_NAME_REGEX.exec(entityName);
if (match == null) {
return err(
`Invalid entity name: \`${entityName}\`. Expected \`ParentType\` or \`ParentType.fieldName\`.`,
);
switch (resolved.kind) {
case "NamedType": {
const astNode = resolved.type.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of type in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "Field": {
const astNode = resolved.field.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of field in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "InputField": {
const astNode = resolved.inputField.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of input field in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "EnumValue": {
const astNode = resolved.enumValue.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of enum value in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "FieldArgument": {
const astNode = resolved.fieldArgument.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of field argument in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "Directive": {
const astNode = resolved.directive.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of directive in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
case "DirectiveArgument": {
const astNode = resolved.directiveArgument.astNode;
if (astNode == null) {
throw new Error(
`Grats bug: Cannot find location of directive argument in coordinate \`${coordinate}\`.`,
);
}
return ok(nullThrows(astNode.name.loc));
}
default: {
// Exhaustive check — if new schema coordinate kinds are added,
// TypeScript will catch this.
const _exhaustive: never = resolved;
throw new Error(
`Grats bug: Unexpected schema coordinate kind: ${(resolved as { kind: string }).kind}`,
);
}
}
const parent = match[1];
const field = match[2] || null;
return ok({ parent, field });
}
5 changes: 4 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ program

program
.command("locate")
.argument("<ENTITY>", "GraphQL entity to locate. E.g. `User` or `User.id`")
.argument(
"<COORDINATE>",
"Schema coordinate to locate. E.g. `User`, `User.name`, `Query.user(id:)`, `@deprecated`",
)
.option(
"--tsconfig <TSCONFIG>",
"Path to tsconfig.json. Defaults to auto-detecting based on the current working directory",
Expand Down
6 changes: 6 additions & 0 deletions src/tests/fixtures/locate/enumValue.invalid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Locate: Greeting.HELLO
/** @gqlEnum */
export enum Greeting {
HELLO = "HELLO",
GOODBYE = "GOODBYE",
}
23 changes: 23 additions & 0 deletions src/tests/fixtures/locate/enumValue.invalid.ts.expected.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# locate/enumValue.invalid.ts

## Input

```ts title="locate/enumValue.invalid.ts"
// Locate: Greeting.HELLO
/** @gqlEnum */
export enum Greeting {
HELLO = "HELLO",
GOODBYE = "GOODBYE",
}
```

## Output

### Error Report

```text
src/tests/fixtures/locate/enumValue.invalid.ts:4:11 - error: Located here

4 HELLO = "HELLO",
~~~~~~~
```
8 changes: 8 additions & 0 deletions src/tests/fixtures/locate/fieldArgument.invalid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Locate: Query.greeting(salutation:)
/** @gqlType */
type Query = unknown;

/** @gqlQueryField */
export function greeting(salutation: string): string {
return `${salutation}, world!`;
}
25 changes: 25 additions & 0 deletions src/tests/fixtures/locate/fieldArgument.invalid.ts.expected.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# locate/fieldArgument.invalid.ts

## Input

```ts title="locate/fieldArgument.invalid.ts"
// Locate: Query.greeting(salutation:)
/** @gqlType */
type Query = unknown;

/** @gqlQueryField */
export function greeting(salutation: string): string {
return `${salutation}, world!`;
}
```

## Output

### Error Report

```text
src/tests/fixtures/locate/fieldArgument.invalid.ts:6:26 - error: Located here

6 export function greeting(salutation: string): string {
~~~~~~~~~~
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export type Date = string;
### Error Locating Type

```text
Cannot locate field `name` on type `Date`. Only object types, interfaces, and input objects have fields.
Invalid schema coordinate: `Date.name`. Expected "Date" to be an Enum, Input Object, Object or Interface type.
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ type User = {
### Error Locating Type

```text
Invalid entity name: `User->name`. Expected `ParentType` or `ParentType.fieldName`.
Invalid schema coordinate: `User->name`. Syntax Error: Invalid character: "-".
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ type User = {
### Error Locating Type

```text
Cannot locate field `not_a_field` on type `User`.
Could not resolve schema coordinate: `User.not_a_field`.
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ type User = {
### Error Locating Type

```text
Cannot locate type `WhoopsNotARealType`.
Could not resolve schema coordinate: `WhoopsNotARealType`.
```
24 changes: 18 additions & 6 deletions website/docs/01-getting-started/02-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,30 +56,42 @@ Options:
-h, --help display help for command

Commands:
locate [options] <ENTITY>
locate [options] <COORDINATE>
```

## Locate

The `locate` command reports the location (file, line, column) at which a given type or field is defined in your code. `grats locate` can also be invoked by other tools. For example the click-to-definition feature of an GraphQL editor integration could use invoke this command to find the location of a type or field.
The `locate` command reports the location (file, line, column) at which a given schema element is defined in your code. It accepts a [Schema Coordinate](https://spec.graphql.org/draft/#sec-Schema-Coordinates) as its argument.

`grats locate` can also be invoked by other tools. For example the click-to-definition feature of a GraphQL editor integration could invoke this command to find the location of a type or field.

For example, Relay's VSCode Extension is [exploring](https://github.com/facebook/relay/pull/4434) adding the ability to leverage such a tool.

```bash
# Locate a named type
npx grats locate User

# Locate a field
npx grats locate User.name

# Locate a named type
npx grats locate User
# Locate a field argument
npx grats locate "Query.user(id:)"

# Locate an enum value
npx grats locate "MyEnum.VALUE"

# Locate a directive
npx grats locate @deprecated
```

### Options

```
Usage: grats locate [options] <ENTITY>
Usage: grats locate [options] <COORDINATE>

Arguments:
ENTITY GraphQL entity to locate. E.g. `User` or `User.id`
COORDINATE Schema coordinate to locate. E.g. `User`, `User.name`,
`Query.user(id:)`, `@deprecated`

Options:
--tsconfig <TSCONFIG> Path to tsconfig.json. Defaults to auto-detecting based on the current working directory
Expand Down