Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## main

- Add `resgraph tools find-definition SomeType.someField` command.

## 1.0.1

- Fix regression where `gql.field` wasn't working properly with all `let` bindings.
Expand Down
65 changes: 60 additions & 5 deletions cli/Cli.res
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,28 @@ Available commands:
init | Initializes a new project.
build | Builds the project.
watch | Builds the project and watches for changes.
tools | Show available ResGraph tools.
help   | Show this help message.
`

let toolsHelpText = `
Tools commands:

find-definition <TypeName[.fieldName]> [--json]

# Useful for finding the location of where a type or field is defined in the source code.
# By default, the output is a human-readable string.
# With the \`--json\` flag, the output is a JSON string with the file path and location of the definition.
`

let parseFindDefinitionArgs = args =>
switch args {
| list{target} => Some((target, false))
| list{target, "--json"} => Some((target, true))
| list{"--json", target} => Some((target, true))
| _ => None
}

let validateConfig = config => {
let issues = []
config->InitProject.validateConfig(~issues)
Expand All @@ -34,6 +53,34 @@ let validateConfig = config => {
}
}

let printFindDefinition = (~target, ~jsonOutput) => {
let config = switch Utils.readConfigFromCwd() {
| Error(msg) => panic(msg)
| Ok(config) => config
}

validateConfig(config)

switch Utils.callPrivateCli(FindDefinition({filePath: config.src, definitionHint: target})) {
| FindDefinition({item: Some(item), error: None}) =>
if jsonOutput {
Console.log(item->Utils.stringifyFindDefinitionJson)
} else {
Console.log(item->Utils.formatFindDefinitionText)
}
| FindDefinition({item: None, error: Some(error)}) =>
if jsonOutput {
Console.log(Utils.stringifyFindDefinitionError(error))
} else {
Console.error(error)
}
Process.process->Process.exitWithCode(1)
| _ =>
Console.error("Unexpected response from ResGraph tools command.")
Process.process->Process.exitWithCode(1)
}
}

try {
switch argsList {
| list{"init"} =>
Expand Down Expand Up @@ -66,7 +113,7 @@ try {
GenerateSchema({src: config.src, outputFolder: config.outputFolder, dumpSchemaSdl: true}),
)
switch res {
| Completion(_) | Hover(_) | Definition(_) | NotInitialized => ()
| Completion(_) | Hover(_) | Definition(_) | FindDefinition(_) | NotInitialized => ()
| Success(_) =>
let buildDuration = performance->now -. timeStart
printBuildTime(buildDuration)
Expand Down Expand Up @@ -103,11 +150,19 @@ try {
~config,
)
Console.log("Watching for changes...")
| list{"lsp", configFilePath} => Lsp.start(~configFilePath, ~mode=Lsp.Stdio)
| list{"lsp", configFilePath} => Lsp.start(~configFilePath, ~mode=Lsp.Stdio)
| list{"tools", "find-definition", ...rest} =>
switch parseFindDefinitionArgs(rest) {
| Some((target, jsonOutput)) => printFindDefinition(~target, ~jsonOutput)
| None =>
Console.error("Invalid tools arguments.")
Console.log(toolsHelpText)
Process.process->Process.exitWithCode(1)
}
| list{"help"} => Console.log(helpText)
| v =>
Console.log("Invalid command: " ++ v->List.toArray->Array.join(" "))
Console.log(helpText)
| v =>
Console.log("Invalid command: " ++ v->List.toArray->Array.join(" "))
Console.log(helpText)
}
} catch {
| Exn.Error(_) => Console.error("Error")
Expand Down
51 changes: 51 additions & 0 deletions cli/Utils.res
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type privateCliCall =
| Hover({filePath: string, position: LspProtocol.loc})
| HoverGraphQL({filePath: string, hoverHint: string})
| Definition({filePath: string, definitionHint: string})
| FindDefinition({filePath: string, definitionHint: string})

let privateCliCallToArgs = call =>
switch call {
Expand Down Expand Up @@ -47,8 +48,26 @@ let privateCliCallToArgs = call =>
]
| HoverGraphQL({filePath, hoverHint}) => ["hover-graphql", filePath, hoverHint]
| Definition({filePath, definitionHint}) => ["definition-graphql", filePath, definitionHint]
| FindDefinition({filePath, definitionHint}) => ["find-definition", filePath, definitionHint]
}

type analyzePosition = {
line: int,
column: int,
}

type analyzeRange = {
start: analyzePosition,
@as("end") end_: analyzePosition,
}

type findDefinitionItem = {
path: string,
kind: string,
file: string,
range: analyzeRange,
}

type generateError = {
file: string,
message: string,
Expand All @@ -63,6 +82,7 @@ type callResult =
| Completion({items: array<LspProtocol.completionItem>})
| Hover({item: LspProtocol.hover})
| Definition({item: LspProtocol.definition})
| FindDefinition({item: option<findDefinitionItem>, error: option<string>})

external toCallResult: string => callResult = "JSON.parse"

Expand Down Expand Up @@ -94,6 +114,37 @@ let callPrivateCli = command => {
->toCallResult
}

let formatFindDefinitionText = (item: findDefinitionItem) => {
let {path, kind, file, range: {start, end_}} = item

`path: ${path}\nkind: ${kind}\nfile: ${file}\nrange: ${start.line->Int.toString}:${start.column->Int.toString}-${end_.line->Int.toString}:${end_.column->Int.toString}`
}

type findDefinitionJson = {
path: string,
kind: string,
file: string,
range: analyzeRange,
}

type findDefinitionErrorJson = {error: string}

let stringifyFindDefinitionJson = (item: findDefinitionItem) => {
let payload: findDefinitionJson = {
path: item.path,
kind: item.kind,
file: item.file,
range: item.range,
}

payload->JSON.stringifyAny
}

let stringifyFindDefinitionError = (error: string) => {
let payload: findDefinitionErrorJson = {error: error}
payload->JSON.stringifyAny
}

let getLastBuiltFromCompilerLog = compilerLogPath => {
let compilerLogContents = compilerLogPath->Fs.readFileSync->Buffer.toString->String.split(Os.eol)

Expand Down
155 changes: 155 additions & 0 deletions src/ml/Analyze.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
open GenerateSchemaTypes

type position = {line : int; column : int}
type range = {start : position; end_ : position}

type definition = {
path : string;
kind : string;
file : string;
range : range;
}

let wrap_in_quotes = Protocol.wrapInQuotes

let stringify_position {line; column} =
Printf.sprintf {|{"line":%d,"column":%d}|} line column

let stringify_range {start; end_} =
Printf.sprintf {|{"start":%s,"end":%s}|} (stringify_position start)
(stringify_position end_)

let stringify_definition {path; kind; file; range} =
Protocol.stringifyObject
[
("path", Some (wrap_in_quotes path));
("kind", Some (wrap_in_quotes kind));
("file", Some (wrap_in_quotes file));
("range", Some (stringify_range range));
]

let stringify_response ?item ?error () =
Protocol.stringifyObject
[
("status", Some (wrap_in_quotes "FindDefinition"));
("item", item |> Option.map stringify_definition);
("error", error |> Option.map wrap_in_quotes);
]

let position_of_lexing pos =
let line, column = Pos.ofLexing pos in
{line = line + 1; column = column + 1}

let range_of_loc (loc : Location.t) =
{start = position_of_lexing loc.loc_start; end_ = position_of_lexing loc.loc_end}

let make_definition ~path ~kind ~fileUri ~loc =
{
path;
kind;
file = fileUri |> Uri.toPath;
range = range_of_loc loc;
}

let load_schema_state ~path =
match Packages.getPackage ~uri:(Uri.fromPath path) with
| None ->
Error
(Printf.sprintf "Path \"%s\" is not inside a ReScript project." path)
| Some package ->
if not (GenerateSchemaUtils.stateFileExists package) then
Error
(Printf.sprintf
"No ResGraph state file was found for this project. Run `resgraph \
build` first."
)
else
try Ok (GenerateSchemaUtils.readStateFile ~package |> fst)
with _ ->
Error
"ResGraph could not read the generated schema state. Run `resgraph \
build` again."

let resolve_type_definition ~schemaState ~definitionHint typename =
match
Hover.findGqlType
(typename |> GenerateSchemaUtils.uncapitalizeFirstChar)
~schemaState
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep uppercase-backed schema IDs in type lookup

resolve_type_definition lowercases the first character before calling Hover.findGqlType, but findGqlType only retries the original capitalization for schemaState.types (src/ml/Hover.ml:12-39). Valid GraphQL types whose stored ID starts uppercase—such as module-defined scalars like Timestamp (src/ml/GenerateSchema.ml:1300-1359) and synthetic input objects like FindShopInputByAddress (src/ml/GenerateSchema.ml:878-900)—therefore fall through to None, so resgraph tools find-definition Timestamp reports that the type does not exist even though it is present in the generated schema.

Useful? React with 👍 / 👎.

with
| Some (Hover.Scalar {typeLocation = {fileUri; loc}}) ->
Ok (make_definition ~path:definitionHint ~kind:"scalar" ~fileUri ~loc)
| Some (Hover.ObjectType {syntheticTypeLocation = Some {fileUri; loc}}) ->
Ok
(make_definition ~path:definitionHint ~kind:"objectType" ~fileUri ~loc)
| Some (Hover.ObjectType {typeLocation = Some (Concrete {fileUri; loc})}) ->
Ok
(make_definition ~path:definitionHint ~kind:"objectType" ~fileUri ~loc)
| Some (Hover.Interface {typeLocation = {fileUri; loc}}) ->
Ok (make_definition ~path:definitionHint ~kind:"interface" ~fileUri ~loc)
| Some (Hover.Enum {typeLocation = Concrete {fileUri; loc}}) ->
Ok (make_definition ~path:definitionHint ~kind:"enum" ~fileUri ~loc)
| Some (Hover.InputObject {syntheticTypeLocation = Some {fileUri; loc}}) ->
Ok
(make_definition ~path:definitionHint ~kind:"inputObject" ~fileUri ~loc)
| Some (Hover.InputObject {typeLocation = Some {fileUri; loc}}) ->
Ok
(make_definition ~path:definitionHint ~kind:"inputObject" ~fileUri ~loc)
| Some (Hover.Union {typeLocation = Concrete {fileUri; loc}}) ->
Ok (make_definition ~path:definitionHint ~kind:"union" ~fileUri ~loc)
| Some (Hover.InputUnion {typeLocation = {fileUri; loc}}) ->
Ok
(make_definition ~path:definitionHint ~kind:"inputUnion" ~fileUri ~loc)
| Some _ ->
Error
(Printf.sprintf
"GraphQL type `%s` exists, but ResGraph could not determine a source \
definition for it."
typename)
| None -> Error (Printf.sprintf "Could not find GraphQL type `%s`." typename)

let resolve_field_definition ~schemaState ~definitionHint typename fieldName =
match
Hover.findGqlType
(typename |> GenerateSchemaUtils.uncapitalizeFirstChar)
~schemaState
with
| Some (Hover.ObjectType {fields} | Hover.Interface {fields} | Hover.InputObject {fields})
-> (
match fields |> List.find_opt (fun (field : gqlField) -> field.name = fieldName) with
| None ->
Error
(Printf.sprintf "Could not find field `%s` on GraphQL type `%s`."
fieldName typename)
| Some {resolverStyle = Resolver _; loc; fileUri} ->
Ok (make_definition ~path:definitionHint ~kind:"resolver" ~fileUri ~loc)
| Some {resolverStyle = Property _; loc; fileUri} ->
Ok
(make_definition ~path:definitionHint ~kind:"exposedField" ~fileUri
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid emitting Location.none as a field definition

This branch returns any stored field location verbatim, but inline-object union members are created with loc = Location.none (src/ml/GenerateSchema.ml:428-445). In that schema shape, resgraph tools find-definition <SyntheticObject>.<field> will print a bogus 1:1-1:1 range in the file instead of either the real declaration site or an explicit “location unavailable” error, which makes the new command misleading for valid inputs.

Useful? React with 👍 / 👎.

~loc))
| Some _ ->
Error
(Printf.sprintf "GraphQL type `%s` does not expose fields." typename)
| None -> Error (Printf.sprintf "Could not find GraphQL type `%s`." typename)

let findDefinition ~path ~definitionHint =
try
match load_schema_state ~path with
| Error error -> stringify_response ~error ()
| Ok schemaState -> (
match definitionHint |> String.split_on_char '.' with
| [typename] -> (
match resolve_type_definition ~schemaState ~definitionHint typename with
| Ok item -> stringify_response ~item ()
| Error error -> stringify_response ~error ())
| [typename; fieldName] -> (
match
resolve_field_definition ~schemaState ~definitionHint typename fieldName
with
| Ok item -> stringify_response ~item ()
| Error error -> stringify_response ~error ())
| _ ->
stringify_response
~error:
"Invalid definition path. Use `TypeName` or `TypeName.fieldName`."
())
with _ -> stringify_response ~error:"ResGraph failed to analyze the definition." ()
3 changes: 3 additions & 0 deletions src/ml/Cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Commands:
hover <path> <line> <col>
hover-graphql <path> <hoverHint>
definition-graphql <path> <definitionHint>
find-definition <path> <definitionHint>
|}

let run_generate ~sourceFolder ~outputFolder ~writeSdlFile =
Expand All @@ -30,6 +31,8 @@ let main () =
Hover.hoverGraphQL ~path ~hoverHint |> print_endline
| [_; "definition-graphql"; path; definitionHint] ->
Hover.definitionGraphQL ~path ~definitionHint |> print_endline
| [_; "find-definition"; path; definitionHint] ->
Analyze.findDefinition ~path ~definitionHint |> print_endline
| args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help
| _ ->
prerr_endline help;
Expand Down
2 changes: 1 addition & 1 deletion src/ml/dune
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
TypeUtils Shared SharedTypes Utils Loc Pos Range Uri Files Packages
ModuleResolution BuildSystem Cache Cfg CmtDirect CmtSummarize
DirectResolve DirectReferences Debug Log PrintType Protocol References
Completion Hover Markdown FindFiles))
Completion Hover Analyze Markdown FindFiles))
Loading