diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index 95e18399..91e1bf79 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -259,12 +259,11 @@ def run(self) -> list[Node]: return AutoAttributeDirective -class desc_js_type_parameter_list(addnodes.desc_type_parameter_list): - """Node for a general type parameter list. +class desc_js_type_parameter_list(nodes.Part, nodes.Inline, nodes.FixedTextElement): + """Node for a javascript type parameter list. - As default the type parameters list is written in line with the rest of the signature. - Set ``multi_line_parameter_list = True`` to describe a multi-line type parameters list. - In that case each type parameter will then be written on its own, indented line. + Unlike normal parameter lists, we use angle braces <> as the braces. Based + on sphinx.addnodes.desc_type_parameter_list """ child_text_separator = ", " @@ -273,6 +272,24 @@ def astext(self) -> str: return f"<{nodes.FixedTextElement.astext(self)}>" +def visit_desc_js_type_parameter_list( + self: HTML5Translator, node: nodes.Element +) -> None: + """Define the html/text rendering for desc_js_type_parameter_list. Based on + sphinx.writers.html5.visit_desc_type_parameter_list + """ + self._visit_sig_parameter_list(node, addnodes.desc_parameter, "<", ">") + + +def depart_desc_js_type_parameter_list( + self: HTML5Translator, node: nodes.Element +) -> None: + """Define the html/text rendering for desc_js_type_parameter_list. Based on + sphinx.writers.html5.depart_desc_type_parameter_list + """ + self._depart_sig_parameter_list(node) + + def add_param_list_to_signode(signode: desc_signature, params: str) -> None: paramlist = desc_js_type_parameter_list() for arg in params.split(","): @@ -280,9 +297,15 @@ def add_param_list_to_signode(signode: desc_signature, params: str) -> None: signode += paramlist -def handle_signature( +def handle_typeparams_for_signature( self: JSObject, sig: str, signode: desc_signature, *, keep_callsig: bool ) -> tuple[str, str]: + """Generic function to handle type params in the sig line for interfaces, + classes, and functions. + + For interfaces and classes we don't prefer the look with parentheses so we + also remove them (by setting keep_callsig to False). + """ typeparams = None if "<" in sig and ">" in sig: base, _, rest = sig.partition("<") @@ -291,17 +314,20 @@ def handle_signature( res = JSCallable.handle_signature(cast(JSCallable, self), sig, signode) sig = sig.strip() lastchild = None + # Check for call signature, if present take it off if signode.children[-1].astext().endswith(")"): lastchild = signode.children[-1] signode.remove(lastchild) if typeparams: add_param_list_to_signode(signode, typeparams) + # if we took off a call signature and we want to keep it put it back. if keep_callsig and lastchild: signode += lastchild return res class JSFunction(JSCallable): + """Variant of JSCallable that can take static/async prefixes""" option_spec = { **JSCallable.option_spec, "static": flag, @@ -323,11 +349,14 @@ def get_display_prefix( return result def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: - return handle_signature(self, sig, signode, keep_callsig=True) + return handle_typeparams_for_signature(self, sig, signode, keep_callsig=True) class JSInterface(JSCallable): - """Like a callable but with a different prefix.""" + """An interface directive. + + Based on sphinx.domains.javascript.JSConstructor. + """ allow_nesting = True @@ -338,30 +367,17 @@ def get_display_prefix(self) -> list[Node]: ] def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: - return handle_signature(self, sig, signode, keep_callsig=False) - - -class JSTypeAlias(JSObject): - doc_field_types = [ - JSGroupedField( - "typeparam", - label="Type parameters", - names=("typeparam",), - can_collapse=True, - ) - ] - - def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: - return handle_signature(self, sig, signode, keep_callsig=False) + return handle_typeparams_for_signature(self, sig, signode, keep_callsig=False) class JSClass(JSConstructor): def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: - return handle_signature(self, sig, signode, keep_callsig=True) + return handle_typeparams_for_signature(self, sig, signode, keep_callsig=True) @cache def patch_JsObject_get_index_text() -> None: + """Add our additional object types to the index""" orig_get_index_text = JSObject.get_index_text def patched_get_index_text( @@ -370,8 +386,6 @@ def patched_get_index_text( name, obj = name_obj if self.objtype == "interface": return _("%s() (interface)") % name - if self.objtype == "typealias": - return _("%s (type alias)") % name return orig_get_index_text(self, objectname, name_obj) JSObject.get_index_text = patched_get_index_text # type:ignore[method-assign] @@ -379,8 +393,6 @@ def patched_get_index_text( def auto_module_directive_bound_to_app(app: Sphinx) -> type[Directive]: class AutoModuleDirective(JsDirectiveWithChildren): - """TODO: words here""" - required_arguments = 1 def run(self) -> list[Node]: @@ -399,18 +411,6 @@ def run(self) -> list[Node]: return JsDocSummary -def visit_desc_js_type_parameter_list( - self: HTML5Translator, node: nodes.Element -) -> None: - self._visit_sig_parameter_list(node, addnodes.desc_parameter, "<", ">") - - -def depart_desc_js_type_parameter_list( - self: HTML5Translator, node: nodes.Element -) -> None: - self._depart_sig_parameter_list(node) - - def add_directives(app: Sphinx) -> None: fix_js_make_xref() fix_staticfunction_objtype() @@ -439,4 +439,5 @@ def add_directives(app: Sphinx) -> None: app.add_node( desc_js_type_parameter_list, html=(visit_desc_js_type_parameter_list, depart_desc_js_type_parameter_list), + text=(visit_desc_js_type_parameter_list, depart_desc_js_type_parameter_list), ) diff --git a/sphinx_js/js/convertTopLevel.ts b/sphinx_js/js/convertTopLevel.ts index 1357950f..4d30f207 100644 --- a/sphinx_js/js/convertTopLevel.ts +++ b/sphinx_js/js/convertTopLevel.ts @@ -436,7 +436,7 @@ export class Converter { // be too? return [undefined, (object as DeclarationReflection).children]; } - const kind = ReflectionKind[object.kind]; + const kind = ReflectionKind.singularString(object.kind); const convertFunc = `convert${kind}` as keyof this; if (!this[convertFunc]) { throw new Error(`No known converter for kind ${kind}`); diff --git a/tests/test_build_ts/source/docs/conf.py b/tests/test_build_ts/source/docs/conf.py index 748a4140..62b954c4 100644 --- a/tests/test_build_ts/source/docs/conf.py +++ b/tests/test_build_ts/source/docs/conf.py @@ -11,12 +11,12 @@ js_language = "typescript" from sphinx.util import rst -from sphinx_js.ir import TypeXRefInternal +from sphinx_js.ir import TypeXRef, TypeXRefInternal -def ts_type_xref_formatter(config, xref): +def ts_type_xref_formatter(config, xref: TypeXRef, kind: str) -> str: if isinstance(xref, TypeXRefInternal): name = rst.escape(xref.name) - return f":js:class:`{name}`" + return f":js:{kind}:`{name}`" else: return xref.name diff --git a/tests/test_build_ts/source/module.ts b/tests/test_build_ts/source/module.ts index 3b49ab04..e805f097 100644 --- a/tests/test_build_ts/source/module.ts +++ b/tests/test_build_ts/source/module.ts @@ -25,13 +25,20 @@ export class A { } } -export class Z { - x: number; +/** + * An instance of class A + */ +export let aInstance: A; + +export class Z { + x: T; constructor(a: number, b: number) {} z() {} } +export let zInstance: Z; + /** * Another thing. */ @@ -41,3 +48,22 @@ export const q = { a: "z29", b: 76 }; * Documentation for the interface I */ export interface I {} + +/** + * An instance of the interface + */ +export let interfaceInstance: I = {}; + +/** + * A function with a type parameter! + * + * We'll refer to ourselves: :js:func:`functionWithTypeParam` + * + * @typeParam T The type parameter + * @typeParam S Another type param + * @param z A Z of T + * @returns The x field of z + */ +export function functionWithTypeParam(z: Z): T { + return z.x; +} diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py index bb200f5f..55a260d2 100644 --- a/tests/test_build_ts/test_build_ts.py +++ b/tests/test_build_ts/test_build_ts.py @@ -60,10 +60,10 @@ def test_abstract_extends_and_implements(self): ' *exported from* "class"\n' "\n" " **Extends:**\n" - ' * "ClassDefinition()"\n' + ' * "ClassDefinition"\n' "\n" " **Implements:**\n" - ' * "Interface()"\n' + ' * "Interface"\n' "\n" " I construct.\n", ) @@ -93,7 +93,7 @@ def test_optional_members(self): question marks sticking out of them.""" self._file_contents_eq( "autoclass_interface_optionals", - "interface OptionalThings()\n" + "interface OptionalThings\n" "\n" ' *exported from* "class"\n' "\n" @@ -183,7 +183,7 @@ def test_predicate(self): * **c** (any) Returns: - boolean (typeguard for "ConstructorlessClass()") + boolean (typeguard for "ConstructorlessClass") """ ), ) @@ -235,7 +235,7 @@ class Extension() *exported from* "class" **Extends:** - * "Base()" + * "Base" Extension.g() @@ -249,76 +249,110 @@ def test_automodule(self): "automodule", dedent( """\ - module.a +module.a - type: 7 + type: 7 - The thing. + The thing. - module.q +module.aInstance - type: { a: string; b: number; } + type: "A" - Another thing. + An instance of class A - async module.f() +module.interfaceInstance - Clutches the bundle + type: "I" - Returns: - Promise + An instance of the interface - module.z(a, b) +module.q - Arguments: - * **a** (number) + type: { a: string; b: number; } - * **b** ({ a: string; b: number; }) + Another thing. - Returns: - number +module.zInstance - class module.A() + type: "Z"<"A"> - This is a summary. This is more info. +async module.f() - *exported from* "module" + Clutches the bundle - A.[Symbol․iterator]() + Returns: + Promise - async A.f() +module.functionWithTypeParam(z) - Returns: - Promise + A function with a type parameter! - A.g(a) + We'll refer to ourselves: "functionWithTypeParam()" - Arguments: - * **a** (number) + Type parameters: + **T** -- The type parameter (extends "A") - Returns: - number + Arguments: + * **z** ("Z") -- A Z of T - class module.Z(a, b) + Returns: + T -- The x field of z - *exported from* "module" +module.z(a, b) - Arguments: - * **a** (number) + Arguments: + * **a** (number) - * **b** (number) + * **b** ({ a: string; b: number; }) - Z.x + Returns: + number - type: number +class module.A() + + This is a summary. This is more info. + + *exported from* "module" + + A.[Symbol․iterator]() + + async A.f() + + Returns: + Promise + + A.g(a) + + Arguments: + * **a** (number) + + Returns: + number + +class module.Z(a, b) + + *exported from* "module" + + Type parameters: + **T** -- + + Arguments: + * **a** (number) + + * **b** (number) + + Z.x + + type: T - Z.z() + Z.z() - interface module.I() +interface module.I - Documentation for the interface I + Documentation for the interface I - *exported from* "module" + *exported from* "module" """ ), ) @@ -354,18 +388,18 @@ def get_links(id): assert href.attrs["href"] == "autoclass_interface_optionals.html#OptionalThings" assert href.attrs["title"] == "OptionalThings" assert next(href.children).name == "code" - assert href.get_text() == "OptionalThings()" + assert href.get_text() == "OptionalThings" href = links[2] assert href.attrs["class"] == ["reference", "internal"] assert ( href.attrs["href"] == "autoclass_constructorless.html#ConstructorlessClass" ) - assert href.get_text() == "ConstructorlessClass()" + assert href.get_text() == "ConstructorlessClass" thunk_links = get_links("thunk") - assert thunk_links[1].get_text() == "OptionalThings()" - assert thunk_links[2].get_text() == "ConstructorlessClass()" + assert thunk_links[1].get_text() == "OptionalThings" + assert thunk_links[2].get_text() == "ConstructorlessClass" def test_sphinx_link_in_description(self): soup = BeautifulSoup( @@ -385,7 +419,7 @@ def test_autosummary(self): soup = BeautifulSoup(self._file_contents("autosummary"), "html.parser") attrs = soup.find(class_="attributes") rows = list(attrs.find_all("tr")) - assert len(rows) == 2 + assert len(rows) == 5 href = rows[0].find("a") assert href.get_text() == "a" @@ -393,13 +427,13 @@ def test_autosummary(self): assert rows[0].find(class_="summary").get_text() == "The thing." href = rows[1].find("a") - assert href.get_text() == "q" - assert href["href"] == "automodule.html#module.q" - assert rows[1].find(class_="summary").get_text() == "Another thing." + assert href.get_text() == "aInstance" + assert href["href"] == "automodule.html#module.aInstance" + assert rows[1].find(class_="summary").get_text() == "An instance of class A" funcs = soup.find(class_="functions") rows = list(funcs.find_all("tr")) - assert len(rows) == 2 + assert len(rows) == 3 row0 = list(rows[0].children) NBSP = "\xa0" assert row0[0].get_text() == f"async{NBSP}f()" @@ -408,7 +442,7 @@ def test_autosummary(self): assert href["href"] == "automodule.html#module.f" assert rows[0].find(class_="summary").get_text() == "Clutches the bundle" - row1 = list(rows[1].children) + row1 = list(rows[2].children) assert row1[0].get_text() == "z(a, b)" href = row1[0].find("a") assert href.get_text() == "z" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 6b36acd7..6c16fb69 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,20 +1,28 @@ from textwrap import dedent, indent +from typing import Any import pytest from sphinx.util import rst from sphinx_js.ir import ( + Attribute, + Class, DescriptionCode, DescriptionText, Exc, Function, + Interface, Param, Return, TypeParam, TypeXRefExternal, TypeXRefInternal, ) -from sphinx_js.renderers import AutoFunctionRenderer, render_description +from sphinx_js.renderers import ( + AutoAttributeRenderer, + AutoFunctionRenderer, + render_description, +) def setindent(txt): @@ -51,10 +59,10 @@ def test_render_description(): ) -def ts_xref_formatter(config, xref): +def ts_xref_formatter(config, xref, kind): if isinstance(xref, TypeXRefInternal): name = rst.escape(xref.name) - return f":js:class:`{name}`" + return f":js:{kind}:`{name}`" else: return xref.name @@ -67,20 +75,45 @@ class _config: class _app: config = _config + def lookup_object(self, partial_path: list[str]): + return self.objects[partial_path[-1]] + renderer = AutoFunctionRenderer.__new__(AutoFunctionRenderer) renderer._app = _app renderer._explicit_formal_params = None renderer._content = [] renderer._set_type_xref_formatter(ts_xref_formatter) renderer._add_span = False + renderer.lookup_object = lookup_object.__get__(renderer) + renderer.objects = {} + return renderer + + +@pytest.fixture() +def attribute_renderer(): + class _config: + pass + + class _app: + config = _config + + renderer = AutoAttributeRenderer.__new__(AutoAttributeRenderer) + renderer._app = _app + renderer._explicit_formal_params = None + renderer._content = [] + renderer._set_type_xref_formatter(ts_xref_formatter) + renderer._add_span = False return renderer @pytest.fixture() -def function_render(function_renderer) -> AutoFunctionRenderer: - def function_render(partial_path=None, use_short_name=False, **args): +def function_render(function_renderer) -> Any: + def function_render(partial_path=None, use_short_name=False, objects=None, **args): + if objects is None: + objects = {} if not partial_path: partial_path = ["blah"] + function_renderer.objects = objects return function_renderer.rst( partial_path, make_function(**args), use_short_name ) @@ -88,38 +121,74 @@ def function_render(partial_path=None, use_short_name=False, **args): return function_render -def make_function(**args): - args = ( - dict( - is_abstract=False, - is_optional=False, - is_static=False, - is_async=False, - is_private=False, - name="", - path=[], - filename="", - deppath="", - description="", - line="", - deprecated="", - examples=[], - see_alsos=[], - properties=[], - exported_from=None, - params=[], - exceptions=[], - returns=[], +@pytest.fixture() +def attribute_render(attribute_renderer) -> Any: + def attribute_render(partial_path=None, use_short_name=False, **args): + if not partial_path: + partial_path = ["blah"] + return attribute_renderer.rst( + partial_path, make_attribute(**args), use_short_name ) - | args + + return attribute_render + + +top_level_dict = dict( + name="", + path=[], + filename="", + deppath="", + description="", + line=0, + deprecated="", + examples=[], + see_alsos=[], + properties=[], + exported_from=None, +) + +member_dict = dict( + is_abstract=False, + is_optional=False, + is_static=False, + is_private=False, +) + +members_and_supers_dict = dict(members=[], supers=[]) + +class_dict = ( + top_level_dict + | members_and_supers_dict + | dict(constructor_=None, is_abstract=False, interfaces=[], type_params=[]) +) +interface_dict = top_level_dict | members_and_supers_dict | dict(type_params=[]) +function_dict = ( + top_level_dict + | member_dict + | dict( + is_async=False, + params=[], + exceptions=[], + returns=[], ) - return Function(**args) +) +attribute_dict = top_level_dict | member_dict | dict(type="") + +def make_class(**args): + return Class(**(class_dict | args)) -# 'is_abstract', 'is_optional', 'is_static', 'is_private', 'name', 'path', -# 'filename', 'deppath', 'description', 'line', 'deprecated', 'examples', -# 'see_alsos', 'properties', 'exported_from', 'params', 'exceptions', and -# 'returns' + +def make_interface(**args): + return Interface(**(interface_dict | args)) + + +def make_function(**args): + return Function(**(function_dict | args)) + + +def make_attribute(**args): + return Attribute(**(attribute_dict | args)) DEFAULT_RESULT = ".. js:function:: blah()\n" @@ -209,14 +278,14 @@ def test_func_render_type_params(function_render): assert function_render( params=[Param("a", type="T"), Param("b", type="S")], type_params=[ - TypeParam("T", "", "a type param"), + TypeParam("T", "number", "a type param"), TypeParam("S", "", "second type param"), ], ) == dedent( """\ - .. js:function:: blah(a, b) + .. js:function:: blah(a, b) - :typeparam T: a type param + :typeparam T: a type param (extends **number**) :typeparam S: second type param :param a: :param b: @@ -227,26 +296,32 @@ def test_func_render_type_params(function_render): def test_render_xref(function_renderer: AutoFunctionRenderer): + function_renderer.objects["A"] = make_class() assert ( function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) == ":js:class:`A`" ) + function_renderer.objects["A"] = make_interface() + assert ( + function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) + == ":js:interface:`A`" + ) assert ( function_renderer.render_type( [TypeXRefInternal(name="A", path=["a.", "A"]), "[]"] ) - == r":js:class:`A`\ []" + == r":js:interface:`A`\ []" ) xref_external = TypeXRefExternal("A", "blah", "a.ts", "a.A") assert function_renderer.render_type([xref_external]) == "A" res = [] - def xref_render(config, val): + def xref_render(config, val, kind): res.append([config, val]) - return val.package + "::" + val.name + return f"{val.package}::{val.name}::{kind}" function_renderer._set_type_xref_formatter(xref_render) - assert function_renderer.render_type([xref_external]) == "blah::A" + assert function_renderer.render_type([xref_external]) == "blah::A::None" assert res[0][0] == function_renderer._app.config assert res[0][1] == xref_external @@ -266,6 +341,7 @@ def test_func_render_param_type(function_render): """ ) assert function_render( + objects={"A": make_interface()}, params=[ Param( "a", @@ -278,7 +354,7 @@ def test_func_render_param_type(function_render): .. js:function:: blah(a) :param a: a description - :type a: :js:class:`A` + :type a: :js:interface:`A` """ ) diff --git a/tests/test_typedoc_analysis/source/typedocConfigTest.ts b/tests/test_typedoc_analysis/source/typedocConfigTest.ts new file mode 100644 index 00000000..c90e8c02 --- /dev/null +++ b/tests/test_typedoc_analysis/source/typedocConfigTest.ts @@ -0,0 +1,9 @@ +/** + * Test typedoc config. It should have registered custom tags dockind and alias. + * + * @dockind A + * @dockind B + * @dockind C + * @alias + */ +export const t: number = 7; diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index 32b96a0c..203b9520 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -5,6 +5,7 @@ from sphinx_js.ir import ( Attribute, Class, + Description, DescriptionCode, DescriptionText, Function, @@ -30,12 +31,12 @@ def join_type(t: Type) -> str: return "".join(e.name if isinstance(e, TypeXRef) else e for e in t) -def join_descri(t: Type) -> str: +def join_description(t: Description) -> str: if not t: return "" if isinstance(t, str): return t - return "".join(e.name if isinstance(e, TypeXRef) else e for e in t) + return "".join(e.code if isinstance(e, DescriptionCode) else e.text for e in t) class TestPathSegments(TypeDocTestCase): @@ -501,7 +502,8 @@ def test_constrained_by_key(self): rst = rst.replace("\\", "").replace(" ", " ") assert ":typeparam T: The type of the object" in rst assert ( - ":typeparam K extends string | number | symbol: The type of the key" in rst + ":typeparam K: The type of the key (extends string | number | symbol)" + in rst ) def test_class_constrained(self): @@ -522,7 +524,7 @@ def test_class_constrained(self): a._options = {} rst = a.rst([obj.name], obj) rst = rst.replace("\\ ", "").replace("\\", "").replace(" ", " ") - assert ":typeparam S extends number[]: The type we contain" in rst + assert ":typeparam S: The type we contain (extends number[])" in rst def test_constrained_by_constructor(self): """Make sure ``new ()`` expressions and, more generally, per-property diff --git a/tests/testing.py b/tests/testing.py index 5ad64990..2bc6dd16 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -100,9 +100,6 @@ def setup_class(cls): """Run the TS analyzer over the TypeDoc output.""" super().setup_class() - def should_destructure(sig, p): - return p.name == "destructureThisPlease" - cls.analyzer = TsAnalyzer(cls.json, cls.extra_data, cls._source_dir)