From 8a68aae3b0e2933f6db69a0867b99e4c60e30b98 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 25 Sep 2023 20:56:26 -0700 Subject: [PATCH] Improve typedoc path handling a bit we can find a path relative to the base of the github repo by looking at `sources.url`. This isn't ideal, it would be nice if typedoc just included an absolute path somewhere. On the other hand, it's better than nothing. This also cleans up some dead code that supported old typedoc versions --- sphinx_js/analyzer_utils.py | 2 + sphinx_js/typedoc.py | 88 +++++++---------- .../test_typedoc_analysis.py | 98 +++++++------------ 3 files changed, 74 insertions(+), 114 deletions(-) diff --git a/sphinx_js/analyzer_utils.py b/sphinx_js/analyzer_utils.py index 4b46f2fc..b6c302cd 100644 --- a/sphinx_js/analyzer_utils.py +++ b/sphinx_js/analyzer_utils.py @@ -107,6 +107,8 @@ def dotted_path(segments: list[str]) -> str: into this and construct a full path based on that. """ + if not segments: + return "" segments_without_separators = [ s[:-1] for s in segments[:-1] if s not in ["./", "../"] ] diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index 75771093..ea59ce78 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -10,7 +10,6 @@ from functools import cache, partial from inspect import isclass from json import load -from os.path import basename, relpath, sep, splitext from pathlib import Path from tempfile import NamedTemporaryFile from typing import Annotated, Any, Literal, TypedDict @@ -20,7 +19,7 @@ from sphinx.errors import SphinxError from . import ir -from .analyzer_utils import Command, is_explicitly_rooted, search_node_modules +from .analyzer_utils import Command, search_node_modules from .suffix_tree import SuffixTree __all__ = ["Analyzer"] @@ -109,12 +108,28 @@ def populate_index(self, root: "IndexType") -> "Converter": self._populate_index_inner(root, parent=None, containing_module=[]) return self + def _url_to_filepath(self, url: str) -> list[str]: + if not url: + return [] + # url looks like "https://github.com/project/repo/blob//path/to/file.ts#lineno + entries = url.split("/") + blob_idx = entries.index("blob") + # have to skip blob and hash too + entries = entries[blob_idx + 2 :] + entries[-1] = entries[-1].rsplit(".")[0] + a = Path("/".join(entries)).resolve().relative_to(Path(self.base_dir).resolve()) + entries = ["."] + entries.extend(a.parts) + for i in range(len(entries) - 1): + entries[i] += "/" + return entries + def _populate_index_inner( self, node: "IndexType", parent: "IndexType | None", containing_module: list[str], - filename: str = "", + filepath: list[str] | None = None, ) -> None: if node.id is not None: # 0 is okay; it's the root node. self.index[node.id] = node @@ -122,11 +137,12 @@ def _populate_index_inner( parent_kind = parent.kindString if parent else "" parent_segments = parent.path if parent else [] if node.sources: - filename = node.sources[0].fileName - node.filename = filename - self.compute_path(node, parent_kind, parent_segments, filename) + filepath = self._url_to_filepath(node.sources[0].url) + if filepath: + node.filepath = filepath + self.compute_path(node, parent_kind, parent_segments, filepath) - if node.kindString in ["External module", "Module"]: + if node.kindString == "Module": containing_module = node.path if parent and isinstance(node, Signature): @@ -162,7 +178,7 @@ def _populate_index_inner( child, parent=node, containing_module=containing_module, - filename=filename, + filepath=filepath, ) def compute_path( @@ -170,7 +186,7 @@ def compute_path( node: "IndexType", parent_kind: str, parent_segments: list[str], - filename: str, + filepath: list[str] | None, ) -> None: """Compute the full, unambiguous list of path segments that points to an entity described by a TypeDoc JSON node. @@ -189,16 +205,8 @@ def compute_path( if not node.flags.isStatic and parent_kind == "Class": delimiter = "#" - if ( - parent_kind == "Project" - and node.kindString - not in [ - "Module", - "External module", - ] - and not parent_segments - ): - parent_segments = make_filepath_segments(filename) + filepath2 = filepath or [] + parent_segments = parent_segments or filepath2 segs = node._path_segments(self.base_dir) @@ -275,6 +283,8 @@ def get_object( class Source(BaseModel): fileName: str line: int + character: int = 0 + url: str = "" class DescriptionItem(BaseModel): @@ -342,9 +352,8 @@ class Base(BaseModel): path: list[str] = [] id: int | None kindString: str = "" - originalName: str | None sources: list[Source] = [] - filename: str = "" + filepath: list[str] = [] flags: Flags = Field(default_factory=Flags) def member_properties(self) -> MemberProperties: @@ -397,8 +406,8 @@ def _top_level_properties(self) -> TopLevelPropertiesDict: return dict( name=self.short_name(), path=ir.Pathname(self.path), - filename=basename(self.filename), - deppath=self.filename, + filename="", + deppath="".join(self.filepath), description=self.comment.get_description(), line=self.sources[0].line if self.sources else None, # These properties aren't supported by TypeDoc: @@ -406,7 +415,7 @@ def _top_level_properties(self) -> TopLevelPropertiesDict: examples=self.comment.get_tag_list("example"), see_alsos=[], properties=[], - exported_from=ir.Pathname(make_filepath_segments(self.filename)), + exported_from=ir.Pathname(self.filepath), ) def to_ir( @@ -610,30 +619,14 @@ def to_ir( return result, self.children -def make_filepath_segments(path: str) -> list[str]: - if not is_explicitly_rooted(path): - path = f".{sep}{path}" - segs = path.split(sep) - filename = splitext(segs[-1])[0] - segments = [s + "/" for s in segs[:-1]] + [filename] - return segments - - -class ExternalModule(NodeBase): - kindString: Literal["External module", "Module"] - originalName: str = "" +class Module(NodeBase): + kindString: Literal["Module"] def short_name(self) -> str: return self.name[1:-1] # strip quotes def _path_segments(self, base_dir: str) -> list[str]: - # 'name' contains folder names if multiple folders are passed into - # TypeDoc. It's also got excess quotes. So we ignore it and take - # 'originalName', which has a nice, absolute path. - if not self.originalName: - return [] - rel = relpath(self.originalName, base_dir) - return make_filepath_segments(rel) + return [] class TypeLiteral(NodeBase): @@ -689,14 +682,7 @@ class OtherNode(NodeBase): Node = Annotated[ - Accessor - | Callable - | Class - | ExternalModule - | Interface - | Member - | OtherNode - | TypeLiteral, + Accessor | Callable | Class | Module | Interface | Member | OtherNode | TypeLiteral, Field(discriminator="kindString"), ] diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index 14e61ac5..8609977e 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -47,72 +47,45 @@ def test_top_level_function(self): # function with no params or return value: json = parse( loads( - r"""{ - "id": 0, - "name": "misterRoot", - "children": [ - { - "id": 1, - "name": "\"longnames\"", - "kindString": "External module", - "originalName": "/a/b/c/tests/test_typedoc_analysis/source/longnames.ts", - "children": [ + r""" { - "id": 2, - "name": "foo", - "kindString": "Function", - "signatures": [ - { - "id": 3, - "name": "foo", - "kindString": "Call signature", - "comment": { - "shortText": "Foo function." - }, - "type": { - "type": "intrinsic", - "name": "void" - } - } - ], - "sources": [ - { - "fileName": "longnames.ts", - "line": 4, - "character": 12 - } - ] + "id": 0, + "name": "misterRoot", + "children": [ + { + "id": 1, + "name": "longnames", + "kindString": "Module", + "children": [ + { + "id": 2, + "name": "foo", + "kindString": "Function", + "signatures": [ + { + "id": 3, + "name": "foo", + "kindString": "Call signature", + "comment": {"shortText": "Foo function."}, + "type": {"type": "intrinsic", "name": "void"} + } + ] + } + ], + "sources": [ + { + "fileName": "longnames.ts", + "line": 1, + "url": "blob/commithash/tests/test_typedoc_analysis/source/longnames.ts" + } + ] + } + ] } - ], - "groups": [ - { - "title": "Functions", - "children": [ - 2 - ] - } - ], - "sources": [ - { - "fileName": "longnames.ts", - "line": 1, - "character": 0 - } - ] - } - ], - "groups": [ - { - "title": "External modules", - "children": [ - 1 - ] - } - ] - }""" + """ ) ) - index = Converter("/a/b/c/tests/").populate_index(json).index + index = Converter("./tests/").populate_index(json).index # Things get indexed by ID: function = index[2] assert function.name == "foo" @@ -290,7 +263,6 @@ def test_class1(self): # have the filling of them factored out. assert subclass.name == "EmptySubclass" assert subclass.path == Pathname(["./", "nodes.", "EmptySubclass"]) - assert subclass.filename == "nodes.ts" assert subclass.description == [DescriptionText("An empty subclass")] assert subclass.deprecated is False assert subclass.examples == []