From 58ef19c0eaf5b70bb2273d9316073b549d3a21a5 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 27 Apr 2026 09:59:49 +0200 Subject: [PATCH] Resolve workspace dependencies in editor analysis --- analysis/src/FindFiles.ml | 30 +++++++++++++++++-- tests/analysis_tests/Makefile | 2 ++ .../tests-sourcedirs-dependency/.gitignore | 2 ++ .../tests-sourcedirs-dependency/Makefile | 14 +++++++++ .../dependency/package.json | 11 +++++++ .../dependency/rescript.json | 11 +++++++ .../dependency/src/WorkspaceDep.res | 1 + .../tests-sourcedirs-dependency/package.json | 12 ++++++++ .../tests-sourcedirs-dependency/rescript.json | 12 ++++++++ .../tests-sourcedirs-dependency/src/Main.res | 4 +++ .../src/expected/Main.res.txt | 16 ++++++++++ .../tests-sourcedirs-dependency/test.sh | 21 +++++++++++++ yarn.lock | 17 +++++++++++ 13 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/.gitignore create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/Makefile create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/dependency/package.json create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/dependency/rescript.json create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/dependency/src/WorkspaceDep.res create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/package.json create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/rescript.json create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/src/Main.res create mode 100644 tests/analysis_tests/tests-sourcedirs-dependency/src/expected/Main.res.txt create mode 100755 tests/analysis_tests/tests-sourcedirs-dependency/test.sh diff --git a/analysis/src/FindFiles.ml b/analysis/src/FindFiles.ml index a18b57023c6..9b2fd500849 100644 --- a/analysis/src/FindFiles.ml +++ b/analysis/src/FindFiles.ml @@ -135,6 +135,32 @@ let collectFiles directory = | None -> None | Some res -> Some (modName, SharedTypes.Impl {cmt; res})) +(* Dependency resolution uses the package graph recorded by the build system in + .sourcedirs.json when available. If a package is not listed there, analysis + falls back to walking up node_modules from the project root. *) +let readSourcedirsPackageRoots base = + let sourceDirsFile = base /+ "lib" /+ "bs" /+ ".sourcedirs.json" in + let readPackageEntry = function + | Json.Array [Json.String name; Json.String path] -> + let path = if Filename.is_relative path then base /+ path else path in + Some (name, path) + | _ -> None + in + match Files.readFile sourceDirsFile with + | None -> [] + | Some text -> ( + match Json.parse text with + | None -> [] + | Some json -> ( + match json |> Json.get "pkgs" |> bind Json.array with + | None -> [] + | Some packages -> packages |> List.filter_map readPackageEntry)) + +let findPackageRoot ~base ~sourcedirsPackageRoots name = + match List.assoc_opt name sourcedirsPackageRoots with + | Some path when Files.exists path -> Some path + | _ -> ModuleResolution.resolveNodeModulePath ~startPath:base name + (* returns a list of (absolute path to cmt(i), relative path from base to source file) *) let findProjectFiles ~public ~namespace ~path ~sourceDirectories ~libBs = let dirs = @@ -233,12 +259,12 @@ let findDependencyFiles base config = in let deps = deps @ devDeps in Log.log ("Dependencies: " ^ String.concat " " deps); + let sourcedirsPackageRoots = readSourcedirsPackageRoots base in let depFiles = deps |> List.map (fun name -> let result = - Json.bind - (ModuleResolution.resolveNodeModulePath ~startPath:base name) + Json.bind (findPackageRoot ~base ~sourcedirsPackageRoots name) (fun path -> let rescriptJsonPath = path /+ "rescript.json" in diff --git a/tests/analysis_tests/Makefile b/tests/analysis_tests/Makefile index ea4a1c02e13..0fd0226e6a0 100644 --- a/tests/analysis_tests/Makefile +++ b/tests/analysis_tests/Makefile @@ -4,6 +4,7 @@ test-analysis-binary: make -C tests test make -C tests-generic-jsx-transform test make -C tests-incremental-typechecking test + make -C tests-sourcedirs-dependency test test-reanalyze: make -C tests-reanalyze test @@ -14,6 +15,7 @@ clean: make -C tests clean make -C tests-generic-jsx-transform clean make -C tests-incremental-typechecking clean + make -C tests-sourcedirs-dependency clean make -C tests-reanalyze clean .PHONY: test-analysis-binary test-reanalyze clean test diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/.gitignore b/tests/analysis_tests/tests-sourcedirs-dependency/.gitignore new file mode 100644 index 00000000000..3b4e8dcf930 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/.gitignore @@ -0,0 +1,2 @@ +lib/ +node_modules/ diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/Makefile b/tests/analysis_tests/tests-sourcedirs-dependency/Makefile new file mode 100644 index 00000000000..4d26df85bb1 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/Makefile @@ -0,0 +1,14 @@ +SHELL = /bin/bash + +build: + yarn build + +test: build + ./test.sh + +clean: + yarn clean + +.DEFAULT_GOAL := test + +.PHONY: clean test diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/dependency/package.json b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/package.json new file mode 100644 index 00000000000..daf5d395888 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/package.json @@ -0,0 +1,11 @@ +{ + "name": "@tests/sourcedirs-dependency-lib", + "private": true, + "scripts": { + "build": "rescript build", + "clean": "rescript clean" + }, + "dependencies": { + "rescript": "workspace:^" + } +} diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/dependency/rescript.json b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/rescript.json new file mode 100644 index 00000000000..b715a6f9a02 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/rescript.json @@ -0,0 +1,11 @@ +{ + "name": "@tests/sourcedirs-dependency-lib", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": [{ "module": "commonjs", "in-source": false }], + "suffix": ".res.js" +} diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/dependency/src/WorkspaceDep.res b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/src/WorkspaceDep.res new file mode 100644 index 00000000000..c25b51ea0cc --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/dependency/src/WorkspaceDep.res @@ -0,0 +1 @@ +let valueFromDependency = 1 diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/package.json b/tests/analysis_tests/tests-sourcedirs-dependency/package.json new file mode 100644 index 00000000000..c0a27837683 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/package.json @@ -0,0 +1,12 @@ +{ + "name": "@tests/sourcedirs-dependency", + "private": true, + "scripts": { + "build": "rescript build", + "clean": "rescript clean" + }, + "dependencies": { + "@tests/sourcedirs-dependency-lib": "workspace:*", + "rescript": "workspace:^" + } +} diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/rescript.json b/tests/analysis_tests/tests-sourcedirs-dependency/rescript.json new file mode 100644 index 00000000000..1339c0fc2c2 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/rescript.json @@ -0,0 +1,12 @@ +{ + "name": "@tests/sourcedirs-dependency", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "dependencies": ["@tests/sourcedirs-dependency-lib"], + "package-specs": [{ "module": "commonjs", "in-source": false }], + "suffix": ".res.js" +} diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/src/Main.res b/tests/analysis_tests/tests-sourcedirs-dependency/src/Main.res new file mode 100644 index 00000000000..bcd93851a48 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/src/Main.res @@ -0,0 +1,4 @@ +let _ = WorkspaceDep.valueFromDependency + +// WorkspaceDep.value +// ^com diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/src/expected/Main.res.txt b/tests/analysis_tests/tests-sourcedirs-dependency/src/expected/Main.res.txt new file mode 100644 index 00000000000..4f24b0022f0 --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/src/expected/Main.res.txt @@ -0,0 +1,16 @@ +Complete src/Main.res 2:16 +posCursor:[2:16] posNoWhite:[2:15] Found expr:[2:3->2:21] +Pexp_ident WorkspaceDep.value:[2:3->2:21] +Completable: Cpath Value[WorkspaceDep, value] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[WorkspaceDep, value] +Path WorkspaceDep.value +[{ + "label": "valueFromDependency", + "kind": 12, + "tags": [], + "detail": "int", + "documentation": null + }] + diff --git a/tests/analysis_tests/tests-sourcedirs-dependency/test.sh b/tests/analysis_tests/tests-sourcedirs-dependency/test.sh new file mode 100755 index 00000000000..c58c8e70b9e --- /dev/null +++ b/tests/analysis_tests/tests-sourcedirs-dependency/test.sh @@ -0,0 +1,21 @@ +for file in src/*.res; do + output="$(dirname $file)/expected/$(basename $file).txt" + ../../../_build/install/default/bin/rescript-editor-analysis test $file &> $output + # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. + if [ "$RUNNER_OS" == "Windows" ]; then + perl -pi -e 's/\r\n/\n/g' -- $output + fi +done + +warningYellow='\033[0;33m' +successGreen='\033[0;32m' +reset='\033[0m' + +diff=$(git ls-files --modified src/expected) +if [[ $diff = "" ]]; then + printf "${successGreen}✅ No unstaged tests difference.${reset}\n" +else + printf "${warningYellow}⚠️ There are unstaged differences in tests/! Did you break a test?\n${diff}\n${reset}" + git --no-pager diff src/expected + exit 1 +fi diff --git a/yarn.lock b/yarn.lock index fa47abcd915..e020a60090b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -808,6 +808,23 @@ __metadata: languageName: unknown linkType: soft +"@tests/sourcedirs-dependency-lib@workspace:*, @tests/sourcedirs-dependency-lib@workspace:tests/analysis_tests/tests-sourcedirs-dependency/dependency": + version: 0.0.0-use.local + resolution: "@tests/sourcedirs-dependency-lib@workspace:tests/analysis_tests/tests-sourcedirs-dependency/dependency" + dependencies: + rescript: "workspace:^" + languageName: unknown + linkType: soft + +"@tests/sourcedirs-dependency@workspace:tests/analysis_tests/tests-sourcedirs-dependency": + version: 0.0.0-use.local + resolution: "@tests/sourcedirs-dependency@workspace:tests/analysis_tests/tests-sourcedirs-dependency" + dependencies: + "@tests/sourcedirs-dependency-lib": "workspace:*" + rescript: "workspace:^" + languageName: unknown + linkType: soft + "@tests/tools@workspace:tests/tools_tests": version: 0.0.0-use.local resolution: "@tests/tools@workspace:tests/tools_tests"