Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

include ModuleAlias target id in doc extraction #886

Open
woeps opened this issue Jan 7, 2024 · 9 comments
Open

include ModuleAlias target id in doc extraction #886

woeps opened this issue Jan 7, 2024 · 9 comments

Comments

@woeps
Copy link
Contributor

woeps commented Jan 7, 2024

Currently ModuleAlias is defined in DocExtraction.docItem as:

 | ModuleAlias of {
      id: string;
      docstring: string list;
      name: string;
      items: docItem list;
    }

While this differentiates between a module implementation and an alias, there is no way of knowing if 2 aliases reference the same target.

E.g:

// Root.res
module A = Core.Array // {id: "Root.A", name: "A",...}
module B = Core.Array // {id: "Root.B", name: "B",...}

While the above is obviously bad interface design, it becomes more important if we were to extract docs from several different modules, which alias the same module.
When generating documentation for a complete project we most likely want to be able to avoid duplication.

If we were to run doc extraction on every module (file) in the example below, it would be nice to have an id stating the aliased module is the same as the one from another extracted json.

src/
    Pkg__A.res
    Pkg__B.res
    Pkg.res // aliases Pkg__A & Pkg__B

Therefore we need a way to uniquely identify a target module.

P.S: I guess we could compare identities by content of the json structures, but a simple id string is easier to handle.

@zth
Copy link
Collaborator

zth commented Jan 7, 2024

This sounds reasonable. @aspeddro something you could look into?

@aspeddro
Copy link
Contributor

aspeddro commented Jan 8, 2024

I was thinking about adding a field for the file path.

e.g:

{
  filepath: "file://src/Pkg__A.res"
}

This could be an id?

@woeps
Copy link
Contributor Author

woeps commented Jan 8, 2024

I guess it's good enough?
The only issue I could think of is: If people were to use symlinks in their codebases, it could get tricky.

Edit:
Not knowing about the implications regarding implementing it.. Currently the id is the "access path" starting with the current file/module. (e.g. Root.Child - the same string I'd use to access this module in the codebase) Therefore, I was thinking a targetId could be introduced of the same shema, but the string representing, how I'd access the targeted module directly. (e.g. Child)
Since aliases can also reference submodules, I think just the path is not enough? (e.g. What should be the targetId of this alias:

// Root.res
module X = Helper.Render.Calc2
// Helper.res
module Calc = {
  // ...
}

module Render = {
  module Calc2 = Calc
  // ...
}
  • If I were to access this alias in the codebase, it would be Root.X, which is exactly the extracted id.
  • If I were to access the module directly in the codebase, it would be Helper.Calc, which I suggest to be the newly introduced targetId.

edit2:
I think it's not enough to just use the righthand-side as the targetId since (theoretically) the alias could reference another alias. The righthand-side currently gets resolved to the actual file (somehow?), if the target is a submodule it should be checked weither itself is just an alias: yes -> recurse | no -> use current filename and append it with the path to the submodule.
I updated above example.

@aspeddro
Copy link
Contributor

aspeddro commented Jan 10, 2024

Since aliases can also reference submodules, I think just the path is not enough? (e.g. What should be the targetId of this alias:

The path where the module definition was made. Like go-to definition, LSP feature.

You example:

module X = Helper.Render.Calc2
//                        ^ go to definition here
{
  [2] = {
    result = {
      range = {
        ["end"] = {
          character = 11,
          line = 0
        },
        start = {
          character = 7,
          line = 0
        }
      },
      uri = "file:///tmp/test/src/Helper.res"
    }
  }
}
// Helper.res
module Calc = {
  let a = 1
}

module Render = {
  module Calc2 = Calc
  // ...
}

@woeps
Copy link
Contributor Author

woeps commented Jan 11, 2024

@aspeddro after thinking a bit about it, here's why I'm hesitant towards the idea of using the filename as an id:

  1. ReScript has no notion of namespaces for modules. The most important question docs should answer is "how may I use this". Which in turn, means I want to see, how I can access certain modules. I need the module name for that and the submodule my desired value lives in.
  2. As of now, docs don't include any paths in general.

On the other hand, it may be interesting to add the "metadata" of the path and location(like your goto definition example) to every doc item for downstream tooling consuming the docs. But I think of this as additional information, besides a targetId.

@aspeddro
Copy link
Contributor

The typed parsetree has a id by module

Example:

module Calc = {
  let a = 1
}

module Render = {
  module Calc2 = Calc
  // ...
}


module A = Render.Calc2
module B = Render.Calc2
./node_modules/.bin/bsc -dtypedtree src/Helper.res
[
  structure_item 
    Tstr_module
    Calc/1002
      module_expr 
        Tmod_structure
        [
          structure_item 
            Tstr_value Nonrec
            [
              <def>
                pattern 
                  Tpat_var "a/1003"
                expression 
                  Texp_constant Const_int 1
            ]
        ]
  structure_item 
    Tstr_module
    Render/1004
      module_expr 
        Tmod_structure
        [
          structure_item 
            Tstr_module
            Calc2/1005
              module_expr 
                Tmod_ident "Calc/1002"
        ]
  structure_item 
    Tstr_module
    A/1006
      module_expr 
        Tmod_ident "Render/1004.Calc2"
  structure_item 
    Tstr_module
    B/1007
      module_expr 
        Tmod_ident "Render/1004.Calc2"
]

Here module_binding record https://github.com/ocaml/ocaml/blob/5348fa9e7c040174f895ee10b5b57d4d22bf7490/typing/typedtree.ml#L312-L320

So now we know that, module A and B is alias for same module "Render/1004.Calc2"

@aspeddro
Copy link
Contributor

module A = Render.Calc2
module B = Render.Calc2
module C = Calc

In this case, A, B, and C should have the same targerId, right?

@woeps
Copy link
Contributor Author

woeps commented Jan 15, 2024

module A = Render.Calc2
module B = Render.Calc2
module C = Calc

In this case, A, B, and C should have the same targerId, right?

Yes, I think so.

@woeps
Copy link
Contributor Author

woeps commented Jun 6, 2024

@zth Some time ago, @aspeddro implemented #900, which includes {filepath: "src/A.resi", line: 2, col: 2} in the docgen.
I just verified on tools@0.6.2:

// ModuleAlias.res
module ArrayAlias1 = Core.Array
module ArrayAlias2 = Core.Array
module ExampleModuleAlias = ExampleModule
module ExampleModuleAlias2 =  ExampleModuleAlias
module InlineModule = {
  let x = 42
}
module InlineModuleAlias = InlineModule

docgen json result:

{
  "name": "ModuleAlias",
  "docstrings": [],
  "source": {
    "filepath": "test/ModuleAlias.res",
    "line": 1,
    "col": 1
  },
  "items": [
  {
    "id": "ModuleAlias.ArrayAlias1",
    "kind": "moduleAlias",
    "name": "ArrayAlias1",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 1,
      "col": 8
    },
    "items": []
  },
...
complete json
{
  "name": "ModuleAlias",
  "docstrings": [],
  "source": {
    "filepath": "test/ModuleAlias.res",
    "line": 1,
    "col": 1
  },
  "items": [
  {
    "id": "ModuleAlias.ArrayAlias1",
    "kind": "moduleAlias",
    "name": "ArrayAlias1",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 1,
      "col": 8
    },
    "items": []
  }, 
  {
    "id": "ModuleAlias.ArrayAlias2",
    "kind": "moduleAlias",
    "name": "ArrayAlias2",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 2,
      "col": 8
    },
    "items": []
  }, 
  {
    "id": "ModuleAlias.ExampleModuleAlias",
    "kind": "moduleAlias",
    "name": "ExampleModuleAlias",
    "docstrings": ["This is an example module."],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 3,
      "col": 8
    },
    "items": [
    {
      "id": "ModuleAlias.ExampleModuleAlias.Variants",
      "name": "Variants",
      "kind": "module",
      "docstrings": ["module of variants"],
      "source": {
        "filepath": "test/ExampleModule.res",
        "line": 5,
        "col": 8
      },
      "items": [
      {
        "id": "ModuleAlias.ExampleModuleAlias.Variants.t",
        "kind": "type",
        "name": "t",
        "signature": "type t =\n  | First\n  | Second(int)\n  | Third({hello: string, goodbye: bool})\n  | Fourth(bool, string, int)",
        "docstrings": ["represents the variant"],
        "source": {
          "filepath": "test/ExampleModule.res",
          "line": 13,
          "col": 3
        },
        "detail": 
        {
          "kind": "variant",
          "items": [
          {
            "name": "First",
            "docstrings": ["first variant constructor without payload"],
            "signature": "First"
          }, 
          {
            "name": "Second",
            "docstrings": ["second variant with int payload"],
            "signature": "Second(int)"
          }, 
          {
            "name": "Third",
            "docstrings": ["third constructor with inline record"],
            "signature": "Third({hello: string, goodbye: bool})",
            "payload": {
              "kind": "inlineRecord",
              "fields": [{
                "name": "hello",
                "optional": false,
                "docstrings": ["hello prop"],
                "signature": "string"
              }, {
                "name": "goodbye",
                "optional": false,
                "docstrings": ["goddbye prop"],
                "signature": "bool"
              }]
            }
          }, 
          {
            "name": "Fourth",
            "docstrings": ["with several payload arguments"],
            "signature": "Fourth(bool, string, int)"
          }]
        }
      }]
    }, 
    {
      "id": "ModuleAlias.ExampleModuleAlias.Record",
      "name": "Record",
      "kind": "module",
      "docstrings": ["module to test record docs"],
      "source": {
        "filepath": "test/ExampleModule.res",
        "line": 26,
        "col": 8
      },
      "items": [
      {
        "id": "ModuleAlias.ExampleModuleAlias.Record.sub",
        "kind": "type",
        "name": "sub",
        "signature": "type sub = {x: bool}",
        "docstrings": ["this is a record"],
        "source": {
          "filepath": "test/ExampleModule.res",
          "line": 34,
          "col": 3
        },
        "detail": 
        {
          "kind": "record",
          "items": [{
            "name": "x",
            "optional": false,
            "docstrings": ["representing a bool value"],
            "signature": "bool"
          }]
        }
      }, 
      {
        "id": "ModuleAlias.ExampleModuleAlias.Record.t",
        "kind": "type",
        "name": "t",
        "signature": "type t = {\n  a: string,\n  b: int,\n  optRecord?: sub,\n  optStr?: string,\n}",
        "docstrings": ["this is another record"],
        "source": {
          "filepath": "test/ExampleModule.res",
          "line": 44,
          "col": 3
        },
        "detail": 
        {
          "kind": "record",
          "items": [{
            "name": "a",
            "optional": false,
            "docstrings": ["prop a"],
            "signature": "string"
          }, {
            "name": "b",
            "deprecated": "use sub instead",
            "optional": false,
            "docstrings": ["deprecated prop"],
            "signature": "int"
          }, {
            "name": "optRecord",
            "optional": true,
            "docstrings": ["this is an optional field of another record"],
            "signature": "option<sub>"
          }, {
            "name": "optStr",
            "optional": true,
            "docstrings": ["this is an optional string"],
            "signature": "option<string>"
          }]
        }
      }]
    }, 
    {
      "id": "ModuleAlias.ExampleModuleAlias.Minimal_OptionalRecord",
      "name": "Minimal_OptionalRecord",
      "kind": "module",
      "docstrings": [],
      "source": {
        "filepath": "test/ExampleModule.res",
        "line": 57,
        "col": 8
      },
      "items": [
      {
        "id": "ModuleAlias.ExampleModuleAlias.Minimal_OptionalRecord.t",
        "kind": "type",
        "name": "t",
        "signature": "type t = {optStr?: string}",
        "docstrings": [],
        "source": {
          "filepath": "test/ExampleModule.res",
          "line": 58,
          "col": 3
        },
        "detail": 
        {
          "kind": "record",
          "items": [{
            "name": "optStr",
            "optional": true,
            "docstrings": [],
            "signature": "option<string>"
          }]
        }
      }]
    }]
  }, 
  {
    "id": "ModuleAlias.ExampleModuleAlias2",
    "kind": "moduleAlias",
    "name": "ExampleModuleAlias2",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 4,
      "col": 8
    },
    "items": []
  }, 
  {
    "id": "ModuleAlias.InlineModule",
    "name": "InlineModule",
    "kind": "module",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 5,
      "col": 8
    },
    "items": [
    {
      "id": "ModuleAlias.InlineModule.x",
      "kind": "value",
      "name": "x",
      "signature": "let x: int",
      "docstrings": [],
      "source": {
        "filepath": "test/ModuleAlias.res",
        "line": 6,
        "col": 7
      }
    }]
  }, 
  {
    "id": "ModuleAlias.InlineModuleAlias",
    "kind": "moduleAlias",
    "name": "InlineModuleAlias",
    "docstrings": [],
    "source": {
      "filepath": "test/ModuleAlias.res",
      "line": 8,
      "col": 8
    },
    "items": []
  }]
}

Observations:

  • the generated json does not provide any information which module actually was aliased
  • aliases to Core.Array, to a previous alias (ExampleModuleAlias2) or to an inline module (InlineModuleAlias) (previously defined in the same file) does not contain any items in the generated json
    • whereas alias to ExampleModule contains all items of the aliased module
  • the source record describes the position in the files, the described expression was encountered

I think it's important to have the source record, because it will help to e.g. generate links to the source file.
But, we still need the json structure to state the actual target of the module alias. (aka targetId)

Another question also came to my mind: Should codegen only generate docs for the current package (defined in rescript.json as sources, or should the dependencies also be included?


P.S: I tried several times to find my way around in the codebase thinking about trying to implement such thing on my own, but I feel pretty lost in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants