From 02bcbd49ad0e0283820051d905f5ef3d1998848c Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 24 Aug 2025 10:55:16 +0200 Subject: [PATCH 1/2] more improvements to completions involving modules --- analysis/src/CompletionFrontEnd.ml | 12 +- analysis/src/ProcessCmt.ml | 83 ++++++- .../tests/src/FirstClassModules.res | 51 ++++ .../src/expected/FirstClassModules.res.txt | 234 +++++++++++++++++- 4 files changed, 366 insertions(+), 14 deletions(-) diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 8d4cb06e62..510db09f65 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -1699,6 +1699,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor in let module_expr (iterator : Ast_iterator.iterator) (me : Parsetree.module_expr) = + let processed = ref false in (match me.pmod_desc with | Pmod_ident lid when lid.loc |> Loc.hasPos ~pos:posBeforeCursor -> let lidPath = flattenLidCheckDot lid in @@ -1710,8 +1711,17 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor setResult (Cpath (CPId {loc = lid.loc; path = lidPath; completionContext = Module})) + | Pmod_functor (name, maybeType, body) -> + let oldScope = !scope in + scope := !scope |> Scope.addModule ~name:name.txt ~loc:name.loc; + (match maybeType with + | None -> () + | Some mt -> iterator.module_type iterator mt); + iterator.module_expr iterator body; + scope := oldScope; + processed := true | _ -> ()); - Ast_iterator.default_iterator.module_expr iterator me + if not !processed then Ast_iterator.default_iterator.module_expr iterator me in let module_type (iterator : Ast_iterator.iterator) (mt : Parsetree.module_type) = diff --git a/analysis/src/ProcessCmt.ml b/analysis/src/ProcessCmt.ml index 306d862a09..96601f6e3b 100644 --- a/analysis/src/ProcessCmt.ml +++ b/analysis/src/ProcessCmt.ml @@ -521,6 +521,8 @@ let rec forStructureItem ~(env : SharedTypes.Env.t) ~(exported : Exported.t) (fun {Typedtree.vb_pat; vb_attributes} -> handlePattern vb_attributes vb_pat) bindings; + bindings + |> List.iter (fun {Typedtree.vb_expr} -> scanLetModules ~env vb_expr); !items | Tstr_module {mb_id; mb_attributes; mb_loc; mb_name = name; mb_expr = {mod_desc}} @@ -630,16 +632,14 @@ and forModule ~env mod_desc moduleName = | Tmod_functor (ident, argName, maybeType, resultExpr) -> (match maybeType with | None -> () - | Some t -> ( - match forTreeModuleType ~name:argName.txt ~env t with - | None -> () - | Some kind -> - let stamp = Ident.binding_time ident in - let declared = - ProcessAttributes.newDeclared ~item:kind ~name:argName - ~extent:t.Typedtree.mty_loc ~stamp ~modulePath:NotVisible false [] - in - Stamps.addModule env.stamps stamp declared)); + | Some t -> + let kind = forTypeModule ~name:argName.txt ~env t.mty_type in + let stamp = Ident.binding_time ident in + let declared = + ProcessAttributes.newDeclared ~item:kind ~name:argName + ~extent:argName.loc ~stamp ~modulePath:NotVisible false [] + in + Stamps.addModule env.stamps stamp declared); forModule ~env resultExpr.mod_desc moduleName | Tmod_apply (functor_, _arg, _coercion) -> forModule ~env functor_.mod_desc moduleName @@ -653,6 +653,69 @@ and forModule ~env mod_desc moduleName = let modTypeKind = forTypeModule ~name:moduleName ~env typ in Constraint (modKind, modTypeKind) +(* + Walk a typed expression and register any `let module M = ...` bindings as local + modules in stamps. This makes trailing-dot completion work for aliases like `M.` + that are introduced inside expression scopes. The declared module is marked as + NotVisible (non-exported) and the extent is the alias identifier location so + scope lookups match precisely. +*) +and scanLetModules ~env (e : Typedtree.expression) = + match e.exp_desc with + | Texp_letmodule (id, name, mexpr, body) -> + let stamp = Ident.binding_time id in + let item = forModule ~env mexpr.mod_desc name.txt in + let declared = + ProcessAttributes.newDeclared ~item ~extent:name.loc ~name ~stamp + ~modulePath:NotVisible false [] + in + Stamps.addModule env.stamps stamp declared; + scanLetModules ~env body + | Texp_let (_rf, bindings, body) -> + List.iter (fun {Typedtree.vb_expr} -> scanLetModules ~env vb_expr) bindings; + scanLetModules ~env body + | Texp_apply {funct; args; _} -> + scanLetModules ~env funct; + args + |> List.iter (function + | _, Some e -> scanLetModules ~env e + | _, None -> ()) + | Texp_tuple exprs -> List.iter (scanLetModules ~env) exprs + | Texp_sequence (e1, e2) -> + scanLetModules ~env e1; + scanLetModules ~env e2 + | Texp_match (e, cases, exn_cases, _) -> + scanLetModules ~env e; + let scan_case {Typedtree.c_lhs = _; c_guard; c_rhs} = + (match c_guard with + | Some g -> scanLetModules ~env g + | None -> ()); + scanLetModules ~env c_rhs + in + List.iter scan_case cases; + List.iter scan_case exn_cases + | Texp_function {case; _} -> + let {Typedtree.c_lhs = _; c_guard; c_rhs} = case in + (match c_guard with + | Some g -> scanLetModules ~env g + | None -> ()); + scanLetModules ~env c_rhs + | Texp_try (e, cases) -> + scanLetModules ~env e; + cases + |> List.iter (fun {Typedtree.c_lhs = _; c_guard; c_rhs} -> + (match c_guard with + | Some g -> scanLetModules ~env g + | None -> ()); + scanLetModules ~env c_rhs) + | Texp_ifthenelse (e1, e2, e3Opt) -> ( + scanLetModules ~env e1; + scanLetModules ~env e2; + match e3Opt with + | Some e3 -> scanLetModules ~env e3 + | None -> ()) + | _ -> () + and forStructure ~name ~env strItems = let exported = Exported.init () in let items = diff --git a/tests/analysis_tests/tests/src/FirstClassModules.res b/tests/analysis_tests/tests/src/FirstClassModules.res index 73acf433ca..e86fef02bc 100644 --- a/tests/analysis_tests/tests/src/FirstClassModules.res +++ b/tests/analysis_tests/tests/src/FirstClassModules.res @@ -16,4 +16,55 @@ let someFn = (~ctx: {"someModule": module(SomeModule)}) => { let _ff = SomeModule.doStuff // ^hov + + module M = CompletionFromModule.SomeModule + // M. + // ^com + + // M.g + // ^com + () +} + +// Module type alias + unpack +module type S2 = SomeModule +let testAliasUnpack = (~ctx: {"someModule": module(SomeModule)}) => { + let module(S2) = ctx["someModule"] + // S2. + // ^com + () +} +// Functor param completion +module Functor = (X: SomeModule) => { + // X. + // ^com + let _u = X.doStuff } +// First-class type hover without binding via module pattern +let typeHover = (~ctx: {"someModule": module(SomeModule)}) => { + let v: module(SomeModule) = ctx["someModule"] + // ^hov + () +} +// Nested unpack inside nested module +module Outer = { + let nested = (~ctx: {"someModule": module(SomeModule)}) => { + let module(SomeModule) = ctx["someModule"] + //SomeModule. + // ^com + () + } +} +// Shadowing: inner binding should be used for completion +let shadowing = ( + ~ctx1: {"someModule": module(SomeModule)}, + ~ctx2: {"someModule": module(SomeModule)}, +) => { + let module(SomeModule) = ctx1["someModule"] + { + let module(SomeModule) = ctx2["someModule"] + //SomeModule. + // ^com + () + } +} \ No newline at end of file diff --git a/tests/analysis_tests/tests/src/expected/FirstClassModules.res.txt b/tests/analysis_tests/tests/src/expected/FirstClassModules.res.txt index cb363ef2be..cc0bfcaf1d 100644 --- a/tests/analysis_tests/tests/src/expected/FirstClassModules.res.txt +++ b/tests/analysis_tests/tests/src/expected/FirstClassModules.res.txt @@ -2,9 +2,9 @@ Hover src/FirstClassModules.res 11:16 {"contents": {"kind": "markdown", "value": "```rescript\nmodule type SomeModule = {\n module Inner\n type t = {x: int}\n let foo: t => int\n let doStuff: string => unit\n let doOtherStuff: string => unit\n}\n```"}} Complete src/FirstClassModules.res 13:15 -posCursor:[13:15] posNoWhite:[13:14] Found expr:[10:13->18:1] -posCursor:[13:15] posNoWhite:[13:14] Found expr:[11:2->16:30] -posCursor:[13:15] posNoWhite:[13:14] Found expr:[13:4->16:30] +posCursor:[13:15] posNoWhite:[13:14] Found expr:[10:13->26:1] +posCursor:[13:15] posNoWhite:[13:14] Found expr:[11:2->25:4] +posCursor:[13:15] posNoWhite:[13:14] Found expr:[13:4->25:4] posCursor:[13:15] posNoWhite:[13:14] Found expr:[13:4->13:15] Pexp_ident SomeModule.:[13:4->13:15] Completable: Cpath Value[SomeModule, ""] @@ -47,3 +47,231 @@ Path SomeModule. Hover src/FirstClassModules.res 16:8 {"contents": {"kind": "markdown", "value": "```rescript\nstring => unit\n```"}} +Complete src/FirstClassModules.res 20:7 +posCursor:[20:7] posNoWhite:[20:6] Found expr:[10:13->26:1] +posCursor:[20:7] posNoWhite:[20:6] Found expr:[11:2->25:4] +posCursor:[20:7] posNoWhite:[20:6] Found expr:[16:2->25:4] +posCursor:[20:7] posNoWhite:[20:6] Found expr:[19:2->25:4] +posCursor:[20:7] posNoWhite:[20:6] Found expr:[20:5->25:4] +posCursor:[20:7] posNoWhite:[20:6] Found expr:[20:5->20:7] +Pexp_ident M.:[20:5->20:7] +Completable: Cpath Value[M, ""] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[M, ""] +Path M. +[{ + "label": "t", + "kind": 22, + "tags": [], + "detail": "type t", + "documentation": {"kind": "markdown", "value": "```rescript\ntype t = {name: string}\n```"} + }, { + "label": "getName", + "kind": 12, + "tags": [], + "detail": "t => string", + "documentation": null + }, { + "label": "thisShouldNotBeCompletedFor", + "kind": 12, + "tags": [], + "detail": "unit => string", + "documentation": null + }] + +Complete src/FirstClassModules.res 23:8 +posCursor:[23:8] posNoWhite:[23:7] Found expr:[10:13->26:1] +posCursor:[23:8] posNoWhite:[23:7] Found expr:[11:2->25:4] +posCursor:[23:8] posNoWhite:[23:7] Found expr:[16:2->25:4] +posCursor:[23:8] posNoWhite:[23:7] Found expr:[19:2->25:4] +posCursor:[23:8] posNoWhite:[23:7] Found expr:[23:5->25:4] +posCursor:[23:8] posNoWhite:[23:7] Found expr:[23:5->23:8] +Pexp_ident M.g:[23:5->23:8] +Completable: Cpath Value[M, g] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[M, g] +Path M.g +[{ + "label": "getName", + "kind": 12, + "tags": [], + "detail": "t => string", + "documentation": null + }] + +Complete src/FirstClassModules.res 32:8 +posCursor:[32:8] posNoWhite:[32:7] Found expr:[30:22->35:1] +posCursor:[32:8] posNoWhite:[32:7] Found expr:[31:2->34:4] +posCursor:[32:8] posNoWhite:[32:7] Found expr:[32:5->34:4] +posCursor:[32:8] posNoWhite:[32:7] Found expr:[32:5->32:8] +Pexp_ident S2.:[32:5->32:8] +Completable: Cpath Value[S2, ""] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[S2, ""] +Path S2. +[{ + "label": "Inner", + "kind": 9, + "tags": [], + "detail": "module Inner", + "documentation": null + }, { + "label": "t", + "kind": 22, + "tags": [], + "detail": "type t", + "documentation": {"kind": "markdown", "value": "```rescript\ntype t = {x: int}\n```"} + }, { + "label": "foo", + "kind": 12, + "tags": [], + "detail": "t => int", + "documentation": null + }, { + "label": "doStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }, { + "label": "doOtherStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }] + +Complete src/FirstClassModules.res 38:7 +posCursor:[38:7] posNoWhite:[38:6] Found expr:[38:5->38:7] +Pexp_ident X.:[38:5->38:7] +Completable: Cpath Value[X, ""] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[X, ""] +Path X. +[{ + "label": "Inner", + "kind": 9, + "tags": [], + "detail": "module Inner", + "documentation": null + }, { + "label": "t", + "kind": 22, + "tags": [], + "detail": "type t", + "documentation": {"kind": "markdown", "value": "```rescript\ntype t = {x: int}\n```"} + }, { + "label": "foo", + "kind": 12, + "tags": [], + "detail": "t => int", + "documentation": null + }, { + "label": "doStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }, { + "label": "doOtherStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }] + +Hover src/FirstClassModules.res 44:7 +{"contents": {"kind": "markdown", "value": "```rescript\nmodule type SomeModule = {\n module Inner\n type t = {x: int}\n let foo: t => int\n let doStuff: string => unit\n let doOtherStuff: string => unit\n}\n```"}} + +Complete src/FirstClassModules.res 52:17 +posCursor:[52:17] posNoWhite:[52:16] Found expr:[50:15->55:3] +posCursor:[52:17] posNoWhite:[52:16] Found expr:[51:4->54:6] +posCursor:[52:17] posNoWhite:[52:16] Found expr:[52:6->54:6] +posCursor:[52:17] posNoWhite:[52:16] Found expr:[52:6->52:17] +Pexp_ident SomeModule.:[52:6->52:17] +Completable: Cpath Value[SomeModule, ""] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[SomeModule, ""] +Path SomeModule. +[{ + "label": "Inner", + "kind": 9, + "tags": [], + "detail": "module Inner", + "documentation": null + }, { + "label": "t", + "kind": 22, + "tags": [], + "detail": "type t", + "documentation": {"kind": "markdown", "value": "```rescript\ntype t = {x: int}\n```"} + }, { + "label": "foo", + "kind": 12, + "tags": [], + "detail": "t => int", + "documentation": null + }, { + "label": "doStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }, { + "label": "doOtherStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }] + +Complete src/FirstClassModules.res 65:17 +posCursor:[65:17] posNoWhite:[65:16] Found expr:[58:16->69:1] +posCursor:[65:17] posNoWhite:[65:16] Found expr:[60:2->69:1] +posCursor:[65:17] posNoWhite:[65:16] Found expr:[62:2->68:3] +posCursor:[65:17] posNoWhite:[65:16] Found expr:[64:4->67:6] +posCursor:[65:17] posNoWhite:[65:16] Found expr:[65:6->67:6] +posCursor:[65:17] posNoWhite:[65:16] Found expr:[65:6->65:17] +Pexp_ident SomeModule.:[65:6->65:17] +Completable: Cpath Value[SomeModule, ""] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[SomeModule, ""] +Path SomeModule. +[{ + "label": "Inner", + "kind": 9, + "tags": [], + "detail": "module Inner", + "documentation": null + }, { + "label": "t", + "kind": 22, + "tags": [], + "detail": "type t", + "documentation": {"kind": "markdown", "value": "```rescript\ntype t = {x: int}\n```"} + }, { + "label": "foo", + "kind": 12, + "tags": [], + "detail": "t => int", + "documentation": null + }, { + "label": "doStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }, { + "label": "doOtherStuff", + "kind": 12, + "tags": [], + "detail": "string => unit", + "documentation": null + }] + From 40f67269b7a657286f3bca8399e01ad794c0ab92 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 24 Aug 2025 10:57:15 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b183c1ef2..38785d6af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Dedicated error message for ternary type mismatch. https://github.com/rescript-lang/rescript/pull/7804 - Dedicated error message for passing a braced ident to something expected to be a record. https://github.com/rescript-lang/rescript/pull/7806 - Hint about partial application when missing required argument in function call. https://github.com/rescript-lang/rescript/pull/7807 +- More autocomplete improvements involving modules and module types. https://github.com/rescript-lang/rescript/pull/7795 #### :house: Internal