From f4e83feb4c90dba7f0fa7eabe18575ada7dd63f7 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 26 Sep 2023 16:40:58 -0700 Subject: [PATCH] Add hook to decide whether to destructure argument (#75) This might seem a bit excessive but it's useful for Pyodide not to have to add @destructure tags for all options arguments. --- sphinx_js/__init__.py | 1 + sphinx_js/typedoc.py | 50 +++++++++++++------ tests/test_typedoc_analysis/source/types.ts | 9 ++++ .../test_typedoc_analysis.py | 4 ++ tests/testing.py | 6 ++- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index d56255eb..48955997 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -142,6 +142,7 @@ def setup(app: Sphinx) -> None: ) app.add_config_value("jsdoc_config_path", default=None, rebuild="env") app.add_config_value("ts_xref_formatter", None, "env") + app.add_config_value("ts_should_destructure_arg", None, "env") # We could use a callable as the "default" param here, but then we would # have had to duplicate or build framework around the logic that promotes diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index 83eccab0..6ff00459 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -4,6 +4,7 @@ import pathlib import re import subprocess +import typing from collections import defaultdict from collections.abc import Iterable, Iterator, Sequence from errno import ENOENT @@ -104,9 +105,21 @@ def parse(json: dict[str, Any]) -> "Project": class Converter: - def __init__(self, base_dir: str): + base_dir: str + index: dict[int, "IndexType"] + _should_destructure_arg: typing.Callable[["Signature", "Param"], bool] + + def __init__( + self, + base_dir: str, + should_destructure_arg: typing.Callable[["Signature", "Param"], bool] + | None = None, + ): self.base_dir: str = base_dir self.index: dict[int, IndexType] = {} + if not should_destructure_arg: + should_destructure_arg = lambda sig, param: False + self._should_destructure_arg = should_destructure_arg def populate_index(self, root: "IndexType") -> "Converter": """Create an ID-to-node mapping for all the TypeDoc output nodes. @@ -232,14 +245,20 @@ def convert_all_nodes(self, root: "Project") -> list[ir.TopLevel]: class Analyzer: - def __init__(self, json: "Project", base_dir: str): + def __init__( + self, + json: "Project", + base_dir: str, + should_destructure_arg: typing.Callable[["Signature", "Param"], bool] + | None = None, + ): """ :arg json: The loaded JSON output from typedoc :arg base_dir: The absolute path of the dir relative to which to construct file-path segments of object paths """ - converter = Converter(base_dir).populate_index(json) + converter = Converter(base_dir, should_destructure_arg).populate_index(json) ir_objects = converter.convert_all_nodes(json) self._base_dir = base_dir @@ -253,7 +272,7 @@ def from_disk( json = typedoc_output( abs_source_paths, app.confdir, app.config.jsdoc_config_path, base_dir ) - return cls(json, base_dir) + return cls(json, base_dir, app.config.ts_should_destructure_arg) def get_object( self, @@ -816,20 +835,21 @@ def _destructure_param(self, param: Param) -> list[Param]: ) return result - def _destructure_params(self) -> list[Param]: - destructure_targets = [] - for tag in self.comment.blockTags: - if tag.tag == "@destructure": - destructure_targets = tag.content[0].text.split(" ") - break - - if not destructure_targets: - return self.parameters + def _destructure_params(self, converter: Converter) -> list[Param]: + destructure_targets: list[str] = [] + for tag_content in self.comment.get_tag_list("destructure"): + tag = tag_content[0] + assert isinstance(tag, ir.DescriptionText) + destructure_targets.extend(tag.text.split(" ")) params = [] for p in self.parameters: - if p.name in destructure_targets: + if p.name in destructure_targets or converter._should_destructure_arg( + self, p + ): params.extend(self._destructure_param(p)) + else: + params.append(p) return params def render(self, converter: Converter) -> Iterator[str | ir.TypeXRef]: @@ -866,7 +886,7 @@ def to_ir( # This isn't ideal, but otherwise the coloring is weird. self.name = "[Symbol\u2024" + self.name[1:] self._fix_type_suffix() - params = self._destructure_params() + params = self._destructure_params(converter) # Would be nice if we could statically determine that the function was # defined with `async` keyword but this is probably good enough is_async = isinstance(self.type, ReferenceType) and self.type.name == "Promise" diff --git a/tests/test_typedoc_analysis/source/types.ts b/tests/test_typedoc_analysis/source/types.ts index afd16334..15b088ff 100644 --- a/tests/test_typedoc_analysis/source/types.ts +++ b/tests/test_typedoc_analysis/source/types.ts @@ -181,6 +181,15 @@ export function destructureTest2({ */ export function destructureTest3({ a, b }: { a: string; b: { c: string } }) {} + +/** + * A test for should_destructure_arg + */ +export function destructureTest4(destructureThisPlease: { + /** The 'a' string. */ + a: string; +}) {} + /** * An example with a function as argument * diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index e0a0cd5f..22f62c43 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -611,6 +611,10 @@ def test_destructured(self): obj = self.analyzer.get_object(["destructureTest3"]) assert obj.params[0].name == "options" assert join_type(obj.params[0].type) == "{ a: string; b: { c: string; }; }" + obj = self.analyzer.get_object(["destructureTest4"]) + assert obj.params[0].name == "destructureThisPlease.a" + assert join_type(obj.params[0].type) == "string" + assert obj.params[0].description == [DescriptionText(text="The 'a' string.")] def test_funcarg(self): obj = self.analyzer.get_object(["funcArg"]) diff --git a/tests/testing.py b/tests/testing.py index e9f77310..5663f4e8 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -94,7 +94,11 @@ class TypeDocAnalyzerTestCase(TypeDocTestCase): def setup_class(cls): """Run the TS analyzer over the TypeDoc output.""" super().setup_class() - cls.analyzer = TsAnalyzer(cls.json, cls._source_dir) + + def should_destructure(sig, p): + return p.name == "destructureThisPlease" + + cls.analyzer = TsAnalyzer(cls.json, cls._source_dir, should_destructure) NO_MATCH = object()