diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 065c3915..9653eece 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: with: python-version: "3.x" + # test.py verifies the emitted C++ corpus's doc links with doxygen; + # graphviz supplies the dot binary its graph generation needs. + - run: sudo apt-get update && sudo apt-get install -y doxygen graphviz + # test.py lints the viewer (tsc/eslint/prettier) but does not install; # provide the workspace deps here. - name: Set up Node.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d034c51..3d00e9da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ When you change the language (new attributes, syntax, types, etc.), you must aud python test.py ``` -runs the full test suite: clippy, fmt, the parser/semantic unit tests, `cargo run --example codegen_tests` (which emits the test corpus through every backend and rebuilds the emitted output), and `cargo doc --no-deps -p codegen_tests` (which catches unresolved doc link references in the generated Rust output). The cpp test corpus uses a regular host C++17 compiler (no MSVC ABI required) so CI doesn't need xwin. +runs the full test suite: clippy, fmt, the parser/semantic unit tests, `cargo run --example codegen_tests` (which emits the test corpus through every backend and rebuilds the emitted output), `cargo doc --no-deps -p codegen_tests` (which catches unresolved doc link references in the generated Rust output), and a doxygen pass over the emitted C++ corpus (which catches unresolvable `@ref`s in the rewritten C++ doc links — skipped with a warning if doxygen isn't installed; `nix-shell` provides it via `shell.nix`). The cpp test corpus uses a regular host C++17 compiler (no MSVC ABI required) so CI doesn't need xwin. Formatting relies on a nightly-only rustfmt feature (`imports_granularity`, configured in `rustfmt.toml`), so check it with nightly: diff --git a/codegen_tests/input/doc_self_links.pyxis b/codegen_tests/input/doc_self_links.pyxis new file mode 100644 index 00000000..00ebca5b --- /dev/null +++ b/codegen_tests/input/doc_self_links.pyxis @@ -0,0 +1,24 @@ +//! Self-link testing module. Its own doc links [`Container`] — the module's +//! doc block must keep its links separate from its first item's, since the +//! module's source location is a proxy borrowed from that item. + +/// Test `Self::` links in type docs. +/// +/// Link to a field as [`Self::field`](Self::field), to a method as +/// [`Self::method`](Self::method), to a nested type as +/// [`Self::Nested`](Self::Nested), and to a nested type's member as +/// [`Self::Nested::nested_field`](Self::Nested::nested_field). +pub type Container { + /// A nested type. Its field is [`Self::nested_field`](Self::nested_field). + pub type Nested { + pub nested_field: u16, + }, + + /// A field. + pub field: u32, +} +impl Container { + /// A method. + #[address(0x10)] + pub fn method(&self); +} diff --git a/codegen_tests/input/qualified_links_consumer.pyxis b/codegen_tests/input/qualified_links_consumer.pyxis new file mode 100644 index 00000000..6276142c --- /dev/null +++ b/codegen_tests/input/qualified_links_consumer.pyxis @@ -0,0 +1,7 @@ +/// Test module-qualified doc-links to functions and extern values. +/// +/// See [`shared_function`](qualified_links_provider::shared_function) and +/// [`shared_extern`](qualified_links_provider::shared_extern). +pub type Consumer { + pub _marker: u8, +} diff --git a/codegen_tests/input/qualified_links_provider.pyxis b/codegen_tests/input/qualified_links_provider.pyxis new file mode 100644 index 00000000..c4d8ab02 --- /dev/null +++ b/codegen_tests/input/qualified_links_provider.pyxis @@ -0,0 +1,7 @@ +/// A module with a freestanding function. +#[address(0x100)] +pub fn shared_function(); + +/// An extern value. +#[address(0x200)] +pub extern shared_extern: *mut u32; diff --git a/codegen_tests/output/cpp/include/consts.hpp b/codegen_tests/output/cpp/include/consts.hpp index 3c3c9ba3..db4bb3e3 100644 --- a/codegen_tests/output/cpp/include/consts.hpp +++ b/codegen_tests/output/cpp/include/consts.hpp @@ -10,7 +10,7 @@ namespace consts { enum class Color : ::std::uint8_t; struct Player; - /// The permissive baseline is [`DEFAULT_MASK`](AccessFlags::DEFAULT_MASK). + /// The permissive baseline is [`DEFAULT_MASK`](@ref consts::AccessFlags_DEFAULT_MASK). enum class AccessFlags : ::std::uint32_t { READ = 0x1, WRITE = 0x2, @@ -30,7 +30,7 @@ namespace consts { static_assert(sizeof(AccessFlags) == 0x4); constexpr ::std::uint32_t AccessFlags_DEFAULT_MASK = 3; - /// Defaults to [`DEFAULT`](Color::DEFAULT). + /// Defaults to [`DEFAULT`](@ref consts::Color_DEFAULT). enum class Color : ::std::uint8_t { Red = 0, Green = 1, @@ -40,9 +40,9 @@ namespace consts { constexpr Color Color_DEFAULT = Color::Red; struct alignas(4) Player { - /// New players begin with [`STARTING_GOLD`](Player::STARTING_GOLD) gold, - /// spawn at x=[`SPAWN_X`](Player::SPAWN_X), and can hold up to - /// [`MAX_SLOTS`](Player::Inventory::MAX_SLOTS) items. + /// New players begin with [`STARTING_GOLD`](@ref consts::Player::STARTING_GOLD) gold, + /// spawn at x=[`SPAWN_X`](@ref consts::Player::SPAWN_X), and can hold up to + /// [`MAX_SLOTS`](@ref consts::Player::Inventory::MAX_SLOTS) items. ::std::int32_t health; static constexpr ::std::uint32_t STARTING_GOLD = 500; diff --git a/codegen_tests/output/cpp/include/doc_self_links.hpp b/codegen_tests/output/cpp/include/doc_self_links.hpp new file mode 100644 index 00000000..b7ed1c29 --- /dev/null +++ b/codegen_tests/output/cpp/include/doc_self_links.hpp @@ -0,0 +1,30 @@ +// @generated by pyxis — do not edit +#pragma once + +#include +#include +#include "pyxis_runtime.hpp" + +namespace doc_self_links { + struct Container; + + /// Test `Self::` links in type docs. + /// + /// Link to a field as [`Self::field`](@ref doc_self_links::Container::field), to a method as + /// [`Self::method`](@ref doc_self_links::Container::method), to a nested type as + /// [`Self::Nested`](@ref doc_self_links::Container::Nested), and to a nested type's member as + /// [`Self::Nested::nested_field`](@ref doc_self_links::Container::Nested::nested_field). + struct alignas(4) Container { + /// A field. + ::std::uint32_t field; + /// A method. + void method() const; + + /// A nested type. Its field is [`Self::nested_field`](@ref doc_self_links::Container::Nested::nested_field). + struct Nested { + ::std::uint16_t nested_field; + }; + }; + static_assert(sizeof(Container) == 0x4); + static_assert(alignof(Container) == 4); +} // namespace doc_self_links diff --git a/codegen_tests/output/cpp/include/extern_values.hpp b/codegen_tests/output/cpp/include/extern_values.hpp index 88cb9a6a..e676f36f 100644 --- a/codegen_tests/output/cpp/include/extern_values.hpp +++ b/codegen_tests/output/cpp/include/extern_values.hpp @@ -10,7 +10,7 @@ namespace extern_values { enum class RenderMode : ::std::uint8_t; struct Engine; - /// The active flag set is [`g_active`](DebugFlags::g_active). + /// The active flag set is [`g_active`](@ref extern_values::DebugFlags_get_g_active). enum class DebugFlags : ::std::uint32_t { WIREFRAME = 0x1, OVERDRAW = 0x2, @@ -31,7 +31,7 @@ namespace extern_values { DebugFlags*& DebugFlags_get_g_active(); struct alignas(4) Engine { - /// The live instance is [`g_instance`](Engine::g_instance). + /// The live instance is [`g_instance`](@ref extern_values::Engine::get_g_instance). ::std::uint32_t frame; static Engine*& get_g_instance(); @@ -39,7 +39,7 @@ namespace extern_values { static_assert(sizeof(Engine) == 0x4); static_assert(alignof(Engine) == 4); - /// The active mode is [`g_current`](RenderMode::g_current). + /// The active mode is [`g_current`](@ref extern_values::RenderMode_get_g_current). enum class RenderMode : ::std::uint8_t { Forward = 0, Deferred = 1, @@ -47,7 +47,7 @@ namespace extern_values { static_assert(sizeof(RenderMode) == 0x1); RenderMode*& RenderMode_get_g_current(); - /// The engine singleton, of type [`Engine`]. Advances with [`g_frame_count`]. + /// The engine singleton, of type [`Engine`](@ref extern_values::Engine). Advances with [`g_frame_count`](@ref extern_values::get_g_frame_count). Engine*& get_g_engine(); ::std::uint32_t& get_g_frame_count(); diff --git a/codegen_tests/output/cpp/include/nested_items.hpp b/codegen_tests/output/cpp/include/nested_items.hpp index 87faa165..b0829fbd 100644 --- a/codegen_tests/output/cpp/include/nested_items.hpp +++ b/codegen_tests/output/cpp/include/nested_items.hpp @@ -10,37 +10,37 @@ namespace nested_items { /// A type with nested declarations. /// - /// See [`InnerEnum`], [`InnerType`], [`InnerFlags`], and [`InnerAlias`]. + /// See [`InnerEnum`](@ref nested_items::Outer::InnerEnum), [`InnerType`](@ref nested_items::Outer::InnerType), [`InnerFlags`](@ref nested_items::Outer::InnerFlags), and [`InnerAlias`](@ref nested_items::Outer::InnerAlias). /// - /// You can also qualify them: [`Outer::InnerEnum`], [`Outer::InnerType`]. + /// You can also qualify them: [`Outer::InnerEnum`](@ref nested_items::Outer::InnerEnum), [`Outer::InnerType`](@ref nested_items::Outer::InnerType). struct alignas(4) Outer { ::std::uint32_t field; - /// An enum nested inside [`Outer`]. + /// An enum nested inside [`Outer`](@ref nested_items::Outer). /// - /// Variants: [`InnerEnum::A`], [`InnerEnum::B`], [`InnerEnum::C`]. + /// Variants: [`InnerEnum::A`](@ref nested_items::Outer::InnerEnum::A), [`InnerEnum::B`](@ref nested_items::Outer::InnerEnum::B), [`InnerEnum::C`](@ref nested_items::Outer::InnerEnum::C). enum class InnerEnum : ::std::uint8_t { A = 0, B = 1, C = 2, }; - /// A type nested inside [`Outer`]. + /// A type nested inside [`Outer`](@ref nested_items::Outer). /// - /// Its field is [`InnerType::inner_field`]. + /// Its field is [`InnerType::inner_field`](@ref nested_items::Outer::InnerType::inner_field). struct InnerType { ::std::uint16_t inner_field; }; - /// Bitflags nested inside [`Outer`]. + /// Bitflags nested inside [`Outer`](@ref nested_items::Outer). /// - /// Members: [`InnerFlags::FLAG_A`], [`InnerFlags::FLAG_B`]. + /// Members: [`InnerFlags::FLAG_A`](@ref nested_items::Outer::InnerFlags::FLAG_A), [`InnerFlags::FLAG_B`](@ref nested_items::Outer::InnerFlags::FLAG_B). struct InnerFlags { static constexpr ::std::uint32_t FLAG_A = 1; static constexpr ::std::uint32_t FLAG_B = 2; }; - /// A type alias nested inside [`Outer`]. + /// A type alias nested inside [`Outer`](@ref nested_items::Outer). using InnerAlias = ::std::uint32_t; }; static_assert(sizeof(Outer) == 0x4); diff --git a/codegen_tests/output/cpp/include/qualified_links_consumer.hpp b/codegen_tests/output/cpp/include/qualified_links_consumer.hpp new file mode 100644 index 00000000..478030e9 --- /dev/null +++ b/codegen_tests/output/cpp/include/qualified_links_consumer.hpp @@ -0,0 +1,20 @@ +// @generated by pyxis — do not edit +#pragma once + +#include +#include +#include "pyxis_runtime.hpp" + +namespace qualified_links_consumer { + struct Consumer; + + /// Test module-qualified doc-links to functions and extern values. + /// + /// See [`shared_function`](@ref qualified_links_provider::shared_function) and + /// [`shared_extern`](@ref qualified_links_provider::get_shared_extern). + struct alignas(1) Consumer { + ::std::uint8_t _marker; + }; + static_assert(sizeof(Consumer) == 0x1); + static_assert(alignof(Consumer) == 1); +} // namespace qualified_links_consumer diff --git a/codegen_tests/output/cpp/include/qualified_links_provider.hpp b/codegen_tests/output/cpp/include/qualified_links_provider.hpp new file mode 100644 index 00000000..a73b63c9 --- /dev/null +++ b/codegen_tests/output/cpp/include/qualified_links_provider.hpp @@ -0,0 +1,15 @@ +// @generated by pyxis — do not edit +#pragma once + +#include +#include +#include "pyxis_runtime.hpp" + +namespace qualified_links_provider { + /// An extern value. + ::std::uint32_t*& get_shared_extern(); + + /// A module with a freestanding function. + using shared_function_t = void (*)(); + extern const shared_function_t shared_function; +} // namespace qualified_links_provider diff --git a/codegen_tests/output/cpp/include/world_consumer.hpp b/codegen_tests/output/cpp/include/world_consumer.hpp index c95bc9b8..7cec07b7 100644 --- a/codegen_tests/output/cpp/include/world_consumer.hpp +++ b/codegen_tests/output/cpp/include/world_consumer.hpp @@ -16,7 +16,7 @@ namespace world_consumer { struct WorldConsumer; /// Consumes the world. Doc-links a type from a deeper, un-imported module - /// ([`DeepMarker`](world::deep::marker::DeepMarker)) to exercise cross-module + /// ([`DeepMarker`](@ref world::deep::marker::DeepMarker)) to exercise cross-module /// doc-link resolution: the link destination must be rewritten to an absolute /// crate path so rustdoc resolves it without a `use` import. struct alignas(8) WorldConsumer { diff --git a/codegen_tests/output/cpp/src/_all_headers.cpp b/codegen_tests/output/cpp/src/_all_headers.cpp index d7e9e205..6201e33b 100644 --- a/codegen_tests/output/cpp/src/_all_headers.cpp +++ b/codegen_tests/output/cpp/src/_all_headers.cpp @@ -9,6 +9,7 @@ #include "definition_body.hpp" #include "diamond_inheritance.hpp" #include "doc_comments.hpp" +#include "doc_self_links.hpp" #include "enums.hpp" #include "extern_bindings.hpp" #include "extern_values.hpp" @@ -23,6 +24,8 @@ #include "nested_items.hpp" #include "pinned.hpp" #include "pyxis_runtime.hpp" +#include "qualified_links_consumer.hpp" +#include "qualified_links_provider.hpp" #include "reexport.hpp" #include "reexport_consumer.hpp" #include "self_referential_generics.hpp" diff --git a/codegen_tests/output/cpp/src/doc_self_links.cpp b/codegen_tests/output/cpp/src/doc_self_links.cpp new file mode 100644 index 00000000..d5b9dd0b --- /dev/null +++ b/codegen_tests/output/cpp/src/doc_self_links.cpp @@ -0,0 +1,10 @@ +// @generated by pyxis — do not edit + +#include "doc_self_links.hpp" + +namespace doc_self_links { + void Container::method() const { + using fn_t = void (*)(const void*); + reinterpret_cast(0x10)(this); + } +} // namespace doc_self_links diff --git a/codegen_tests/output/cpp/src/qualified_links_provider.cpp b/codegen_tests/output/cpp/src/qualified_links_provider.cpp new file mode 100644 index 00000000..1b3a18b8 --- /dev/null +++ b/codegen_tests/output/cpp/src/qualified_links_provider.cpp @@ -0,0 +1,12 @@ +// @generated by pyxis — do not edit + +#include "qualified_links_provider.hpp" + +namespace qualified_links_provider { + using shared_function_t = void (*)(); + const shared_function_t shared_function = reinterpret_cast(0x100); + + ::std::uint32_t*& get_shared_extern() { + return *reinterpret_cast<::std::uint32_t**>(0x200); + } +} // namespace qualified_links_provider diff --git a/codegen_tests/output/json/output.json b/codegen_tests/output/json/output.json index 0c91bcc0..bf026829 100644 --- a/codegen_tests/output/json/output.json +++ b/codegen_tests/output/json/output.json @@ -3095,6 +3095,147 @@ "line": 6 } }, + "doc_self_links::Container": { + "path": "doc_self_links::Container", + "visibility": "public", + "size": 4, + "alignment": 4, + "category": "defined", + "kind": { + "type": "type", + "doc": " Test `Self::` links in type docs.\n\n Link to a field as [`Self::field`](Self::field), to a method as\n [`Self::method`](Self::method), to a nested type as\n [`Self::Nested`](Self::Nested), and to a nested type's member as\n [`Self::Nested::nested_field`](Self::Nested::nested_field).", + "doc_links": [ + { + "text": "Self::field", + "target_kind": "item", + "path": "doc_self_links::Container", + "anchor": "field-field" + }, + { + "text": "Self::method", + "target_kind": "item", + "path": "doc_self_links::Container", + "anchor": "func-method" + }, + { + "text": "Self::Nested", + "target_kind": "item", + "path": "doc_self_links::Container::Nested" + }, + { + "text": "Self::Nested::nested_field", + "target_kind": "item", + "path": "doc_self_links::Container::Nested", + "anchor": "field-nested_field" + } + ], + "fields": [ + { + "visibility": "public", + "name": "field", + "doc": " A field.", + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 12, + "line": 18 + } + } + ], + "associated_functions": [ + { + "visibility": "public", + "name": "method", + "doc": " A method.", + "body": { + "type": "address", + "address": 16 + }, + "arguments": [ + { + "type": "const_self" + } + ], + "return_type": null, + "calling_convention": "system", + "source": { + "file_index": 12, + "line": 23 + } + } + ], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false, + "nested_items": [ + "doc_self_links::Container::Nested" + ] + }, + "source": { + "file_index": 12, + "line": 11 + } + }, + "doc_self_links::Container::Nested": { + "path": "doc_self_links::Container::Nested", + "visibility": "public", + "size": 2, + "alignment": 2, + "category": "defined", + "kind": { + "type": "type", + "doc": " A nested type. Its field is [`Self::nested_field`](Self::nested_field).", + "doc_links": [ + { + "text": "Self::nested_field", + "target_kind": "item", + "path": "doc_self_links::Container::Nested", + "anchor": "field-nested_field" + } + ], + "fields": [ + { + "visibility": "public", + "name": "nested_field", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u16" + }, + "offset": 0, + "size": 2, + "alignment": 2, + "is_base": false, + "source": { + "file_index": 12, + "line": 14 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 12, + "line": 13 + } + }, "enums::TestEnum": { "path": "enums::TestEnum", "visibility": "public", @@ -3114,7 +3255,7 @@ "value": 0, "doc": null, "source": { - "file_index": 12, + "file_index": 13, "line": 4 } }, @@ -3123,7 +3264,7 @@ "value": 1, "doc": null, "source": { - "file_index": 12, + "file_index": 13, "line": 5 } }, @@ -3132,7 +3273,7 @@ "value": 2, "doc": null, "source": { - "file_index": 12, + "file_index": 13, "line": 6 } } @@ -3150,7 +3291,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 12, + "file_index": 13, "line": 10 } }, @@ -3173,7 +3314,7 @@ }, "calling_convention": "system", "source": { - "file_index": 12, + "file_index": 13, "line": 13 } } @@ -3185,7 +3326,7 @@ "pinned": false }, "source": { - "file_index": 12, + "file_index": 13, "line": 2 } }, @@ -3211,7 +3352,7 @@ "pinned": false }, "source": { - "file_index": 13, + "file_index": 14, "line": 7 } }, @@ -3238,7 +3379,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 13, + "file_index": 14, "line": 10 } } @@ -3253,7 +3394,7 @@ "pinned": false }, "source": { - "file_index": 13, + "file_index": 14, "line": 9 } }, @@ -3283,7 +3424,7 @@ "value": 1, "doc": null, "source": { - "file_index": 14, + "file_index": 15, "line": 33 } }, @@ -3292,7 +3433,7 @@ "value": 2, "doc": null, "source": { - "file_index": 14, + "file_index": 15, "line": 34 } } @@ -3307,7 +3448,7 @@ ] }, "source": { - "file_index": 14, + "file_index": 15, "line": 29 } }, @@ -3330,7 +3471,7 @@ "address": 20480 }, "source": { - "file_index": 14, + "file_index": 15, "line": 31 } }, @@ -3364,7 +3505,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 14, + "file_index": 15, "line": 16 } } @@ -3382,7 +3523,7 @@ ] }, "source": { - "file_index": 14, + "file_index": 15, "line": 8 } }, @@ -3405,7 +3546,7 @@ "address": 12288 }, "source": { - "file_index": 14, + "file_index": 15, "line": 13 } }, @@ -3435,7 +3576,7 @@ "value": 0, "doc": null, "source": { - "file_index": 14, + "file_index": 15, "line": 24 } }, @@ -3444,7 +3585,7 @@ "value": 1, "doc": null, "source": { - "file_index": 14, + "file_index": 15, "line": 25 } } @@ -3460,7 +3601,7 @@ ] }, "source": { - "file_index": 14, + "file_index": 15, "line": 20 } }, @@ -3483,7 +3624,7 @@ "address": 16384 }, "source": { - "file_index": 14, + "file_index": 15, "line": 22 } }, @@ -3518,7 +3659,7 @@ "address": 8192 }, "source": { - "file_index": 14, + "file_index": 15, "line": 6 } }, @@ -3538,7 +3679,7 @@ "address": 4096 }, "source": { - "file_index": 14, + "file_index": 15, "line": 3 } }, @@ -3565,7 +3706,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 15, + "file_index": 16, "line": 6 } } @@ -3585,7 +3726,7 @@ }, "calling_convention": "system", "source": { - "file_index": 15, + "file_index": 16, "line": 10 } }, @@ -3607,7 +3748,7 @@ }, "calling_convention": "system", "source": { - "file_index": 15, + "file_index": 16, "line": 12 } }, @@ -3629,7 +3770,7 @@ }, "calling_convention": "system", "source": { - "file_index": 15, + "file_index": 16, "line": 14 } } @@ -3643,7 +3784,7 @@ "pinned": false }, "source": { - "file_index": 15, + "file_index": 16, "line": 5 } }, @@ -3676,7 +3817,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 15, + "file_index": 16, "line": 48 } }, @@ -3696,7 +3837,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 15, + "file_index": 16, "line": 49 } } @@ -3723,7 +3864,7 @@ }, "calling_convention": "system", "source": { - "file_index": 15, + "file_index": 16, "line": 53 } }, @@ -3751,7 +3892,7 @@ "Y" ], "source": { - "file_index": 15, + "file_index": 16, "line": 60 } } @@ -3765,7 +3906,7 @@ "pinned": false }, "source": { - "file_index": 15, + "file_index": 16, "line": 47 } }, @@ -3834,7 +3975,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 16, + "file_index": 17, "line": 20 } } @@ -3867,7 +4008,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 16, + "file_index": 17, "line": 24 } } @@ -3881,7 +4022,7 @@ "pinned": false }, "source": { - "file_index": 16, + "file_index": 17, "line": 19 } }, @@ -3908,7 +4049,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 16, + "file_index": 17, "line": 15 } }, @@ -3925,7 +4066,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 16, + "file_index": 17, "line": 16 } } @@ -3940,7 +4081,7 @@ "pinned": false }, "source": { - "file_index": 16, + "file_index": 17, "line": 14 } }, @@ -3963,7 +4104,7 @@ "value": 0, "doc": null, "source": { - "file_index": 17, + "file_index": 18, "line": 2 } } @@ -3981,7 +4122,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 17, + "file_index": 18, "line": 6 } } @@ -3993,7 +4134,7 @@ "pinned": false }, "source": { - "file_index": 17, + "file_index": 18, "line": 1 } }, @@ -4026,7 +4167,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 55 } }, @@ -4043,7 +4184,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 56 } }, @@ -4064,7 +4205,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 50 } } @@ -4079,7 +4220,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 54 } }, @@ -4112,7 +4253,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 63 } } @@ -4127,7 +4268,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 62 } }, @@ -4157,7 +4298,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 72 } } @@ -4195,7 +4336,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 18, + "file_index": 19, "line": 72 } } @@ -4209,7 +4350,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 70 } }, @@ -4262,7 +4403,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 72 } } @@ -4277,7 +4418,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 72 } }, @@ -4310,7 +4451,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 27 } }, @@ -4333,7 +4474,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 28 } }, @@ -4360,7 +4501,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 29 } } @@ -4375,7 +4516,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 26 } }, @@ -4412,7 +4553,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 35 } } @@ -4427,7 +4568,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 34 } }, @@ -4463,7 +4604,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 41 } } @@ -4478,7 +4619,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 40 } }, @@ -4512,7 +4653,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 20 } }, @@ -4532,7 +4673,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 21 } } @@ -4547,7 +4688,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 19 } }, @@ -4574,7 +4715,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 47 } } @@ -4589,7 +4730,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 46 } }, @@ -4622,7 +4763,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 7 } } @@ -4637,7 +4778,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 6 } }, @@ -4670,7 +4811,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 18, + "file_index": 19, "line": 14 } } @@ -4685,7 +4826,7 @@ "pinned": false }, "source": { - "file_index": 18, + "file_index": 19, "line": 13 } }, @@ -4712,7 +4853,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 8 } }, @@ -4729,7 +4870,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 9 } } @@ -4744,7 +4885,7 @@ "pinned": false }, "source": { - "file_index": 19, + "file_index": 20, "line": 7 } }, @@ -4781,7 +4922,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 27 } } @@ -4796,7 +4937,7 @@ "pinned": false }, "source": { - "file_index": 19, + "file_index": 20, "line": 26 } }, @@ -4835,7 +4976,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 33 } } @@ -4850,7 +4991,7 @@ "pinned": false }, "source": { - "file_index": 19, + "file_index": 20, "line": 32 } }, @@ -4883,7 +5024,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 15 } } @@ -4898,7 +5039,7 @@ "pinned": false }, "source": { - "file_index": 19, + "file_index": 20, "line": 14 } }, @@ -4931,7 +5072,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 19, + "file_index": 20, "line": 21 } } @@ -4946,7 +5087,7 @@ "pinned": false }, "source": { - "file_index": 19, + "file_index": 20, "line": 20 } }, @@ -5078,7 +5219,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 24 } }, @@ -5099,7 +5240,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 25 } } @@ -5114,7 +5255,7 @@ "pinned": false }, "source": { - "file_index": 20, + "file_index": 21, "line": 23 } }, @@ -5145,7 +5286,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 4 } } @@ -5160,7 +5301,7 @@ "pinned": false }, "source": { - "file_index": 20, + "file_index": 21, "line": 3 } }, @@ -5187,7 +5328,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 16 } }, @@ -5204,7 +5345,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 17 } }, @@ -5221,7 +5362,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 18 } } @@ -5236,7 +5377,7 @@ "pinned": false }, "source": { - "file_index": 20, + "file_index": 21, "line": 15 } }, @@ -5275,7 +5416,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 20, + "file_index": 21, "line": 10 } } @@ -5290,7 +5431,7 @@ "pinned": false }, "source": { - "file_index": 20, + "file_index": 21, "line": 9 } }, @@ -5317,7 +5458,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 21, + "file_index": 22, "line": 11 } }, @@ -5338,7 +5479,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 21, + "file_index": 22, "line": 8 } } @@ -5353,7 +5494,7 @@ "pinned": false }, "source": { - "file_index": 21, + "file_index": 22, "line": 10 } }, @@ -5380,7 +5521,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 21, + "file_index": 22, "line": 4 } }, @@ -5397,7 +5538,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 21, + "file_index": 22, "line": 5 } }, @@ -5418,7 +5559,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 21, + "file_index": 22, "line": 1 } } @@ -5433,7 +5574,7 @@ "pinned": false }, "source": { - "file_index": 21, + "file_index": 22, "line": 3 } }, @@ -5463,7 +5604,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 4 } }, @@ -5480,7 +5621,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 8 } } @@ -5513,7 +5654,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 12 } } @@ -5547,7 +5688,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 4 } } @@ -5561,7 +5702,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 2 } }, @@ -5611,7 +5752,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 4 } } @@ -5626,7 +5767,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 4 } }, @@ -5653,7 +5794,7 @@ "alignment": 8, "is_base": true, "source": { - "file_index": 22, + "file_index": 23, "line": 23 } }, @@ -5670,7 +5811,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 24 } } @@ -5704,7 +5845,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 12 } }, @@ -5735,7 +5876,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 28 } } @@ -5769,7 +5910,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 18 } }, @@ -5800,7 +5941,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 19 } } @@ -5814,7 +5955,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 16 } }, @@ -5841,7 +5982,7 @@ "alignment": 8, "is_base": true, "source": { - "file_index": 22, + "file_index": 23, "line": 40 } }, @@ -5858,7 +5999,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 41 } } @@ -5892,7 +6033,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 12 } }, @@ -5924,7 +6065,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 28 } }, @@ -5955,7 +6096,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 45 } } @@ -5989,7 +6130,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 34 } }, @@ -6020,7 +6161,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 35 } }, @@ -6051,7 +6192,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 36 } } @@ -6065,7 +6206,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 32 } }, @@ -6092,7 +6233,7 @@ "alignment": 8, "is_base": true, "source": { - "file_index": 22, + "file_index": 23, "line": 58 } }, @@ -6109,7 +6250,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 59 } } @@ -6143,7 +6284,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 12 } }, @@ -6175,7 +6316,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 28 } }, @@ -6207,7 +6348,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 45 } }, @@ -6238,7 +6379,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 63 } } @@ -6272,7 +6413,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 51 } }, @@ -6303,7 +6444,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 52 } }, @@ -6334,7 +6475,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 53 } }, @@ -6365,7 +6506,7 @@ }, "calling_convention": "system", "source": { - "file_index": 22, + "file_index": 23, "line": 54 } } @@ -6379,7 +6520,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 49 } }, @@ -6429,7 +6570,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 51 } }, @@ -6469,7 +6610,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 52 } }, @@ -6509,7 +6650,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 53 } }, @@ -6549,7 +6690,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 54 } } @@ -6564,7 +6705,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 51 } }, @@ -6614,7 +6755,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 34 } }, @@ -6654,7 +6795,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 35 } }, @@ -6694,7 +6835,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 36 } } @@ -6709,7 +6850,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 34 } }, @@ -6759,7 +6900,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 18 } }, @@ -6799,7 +6940,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 22, + "file_index": 23, "line": 19 } } @@ -6814,7 +6955,7 @@ "pinned": false }, "source": { - "file_index": 22, + "file_index": 23, "line": 18 } }, @@ -6873,7 +7014,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 23, + "file_index": 24, "line": 31 } } @@ -6894,7 +7035,7 @@ ] }, "source": { - "file_index": 23, + "file_index": 24, "line": 6 } }, @@ -6920,7 +7061,7 @@ } }, "source": { - "file_index": 23, + "file_index": 24, "line": 29 } }, @@ -6968,7 +7109,7 @@ "value": 0, "doc": null, "source": { - "file_index": 23, + "file_index": 24, "line": 11 } }, @@ -6977,7 +7118,7 @@ "value": 1, "doc": null, "source": { - "file_index": 23, + "file_index": 24, "line": 12 } }, @@ -6986,7 +7127,7 @@ "value": 2, "doc": null, "source": { - "file_index": 23, + "file_index": 24, "line": 13 } } @@ -6999,7 +7140,7 @@ "pinned": false }, "source": { - "file_index": 23, + "file_index": 24, "line": 10 } }, @@ -7041,7 +7182,7 @@ "value": 1, "doc": null, "source": { - "file_index": 23, + "file_index": 24, "line": 25 } }, @@ -7050,7 +7191,7 @@ "value": 2, "doc": null, "source": { - "file_index": 23, + "file_index": 24, "line": 26 } } @@ -7062,7 +7203,7 @@ "pinned": false }, "source": { - "file_index": 23, + "file_index": 24, "line": 24 } }, @@ -7102,7 +7243,7 @@ "alignment": 2, "is_base": false, "source": { - "file_index": 23, + "file_index": 24, "line": 19 } } @@ -7117,7 +7258,7 @@ "pinned": false }, "source": { - "file_index": 23, + "file_index": 24, "line": 18 } }, @@ -7144,7 +7285,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 24, + "file_index": 25, "line": 10 } } @@ -7159,7 +7300,7 @@ "pinned": true }, "source": { - "file_index": 24, + "file_index": 25, "line": 9 } }, @@ -7182,7 +7323,7 @@ "value": 0, "doc": null, "source": { - "file_index": 24, + "file_index": 25, "line": 16 } }, @@ -7191,7 +7332,7 @@ "value": 1, "doc": null, "source": { - "file_index": 24, + "file_index": 25, "line": 17 } } @@ -7204,7 +7345,7 @@ "pinned": true }, "source": { - "file_index": 24, + "file_index": 25, "line": 15 } }, @@ -7227,7 +7368,7 @@ "value": 1, "doc": null, "source": { - "file_index": 24, + "file_index": 25, "line": 23 } }, @@ -7236,7 +7377,7 @@ "value": 2, "doc": null, "source": { - "file_index": 24, + "file_index": 25, "line": 24 } } @@ -7248,7 +7389,7 @@ "pinned": true }, "source": { - "file_index": 24, + "file_index": 25, "line": 22 } }, @@ -7275,7 +7416,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 24, + "file_index": 25, "line": 4 } } @@ -7290,10 +7431,88 @@ "pinned": true }, "source": { - "file_index": 24, + "file_index": 25, "line": 3 } }, + "qualified_links_consumer::Consumer": { + "path": "qualified_links_consumer::Consumer", + "visibility": "public", + "size": 1, + "alignment": 1, + "category": "defined", + "kind": { + "type": "type", + "doc": " Test module-qualified doc-links to functions and extern values.\n\n See [`shared_function`](qualified_links_provider::shared_function) and\n [`shared_extern`](qualified_links_provider::shared_extern).", + "doc_links": [ + { + "text": "qualified_links_provider::shared_function", + "target_kind": "module", + "path": "qualified_links_provider", + "anchor": "func-shared_function" + }, + { + "text": "qualified_links_provider::shared_extern", + "target_kind": "item", + "path": "qualified_links_provider::shared_extern" + } + ], + "fields": [ + { + "visibility": "public", + "name": "_marker", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u8" + }, + "offset": 0, + "size": 1, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 26, + "line": 6 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 26, + "line": 5 + } + }, + "qualified_links_provider::shared_extern": { + "path": "qualified_links_provider::shared_extern", + "visibility": "public", + "size": 0, + "alignment": 1, + "category": "defined", + "kind": { + "type": "extern_value", + "doc": " An extern value.", + "value_type": { + "type": "mut_pointer", + "inner": { + "type": "raw", + "path": "u32" + } + }, + "address": 512 + }, + "source": { + "file_index": 27, + "line": 7 + } + }, "reexport::Bundle": { "path": "reexport::Bundle", "visibility": "public", @@ -7317,7 +7536,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 25, + "file_index": 28, "line": 11 } }, @@ -7334,7 +7553,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 25, + "file_index": 28, "line": 12 } } @@ -7349,7 +7568,7 @@ "pinned": false }, "source": { - "file_index": 25, + "file_index": 28, "line": 10 } }, @@ -7376,7 +7595,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 26, + "file_index": 29, "line": 8 } } @@ -7391,7 +7610,7 @@ "pinned": false }, "source": { - "file_index": 26, + "file_index": 29, "line": 7 } }, @@ -7418,7 +7637,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 20 } }, @@ -7439,7 +7658,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 23 } }, @@ -7462,7 +7681,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 23 } }, @@ -7485,7 +7704,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 24 } }, @@ -7508,7 +7727,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 25 } }, @@ -7529,7 +7748,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 13 } } @@ -7544,7 +7763,7 @@ "pinned": false }, "source": { - "file_index": 27, + "file_index": 30, "line": 19 } }, @@ -7577,7 +7796,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 4 } } @@ -7592,7 +7811,7 @@ "pinned": false }, "source": { - "file_index": 27, + "file_index": 30, "line": 3 } }, @@ -7619,7 +7838,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 31 } }, @@ -7640,7 +7859,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 34 } }, @@ -7663,7 +7882,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 34 } }, @@ -7686,7 +7905,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 35 } } @@ -7701,7 +7920,7 @@ "pinned": false }, "source": { - "file_index": 27, + "file_index": 30, "line": 30 } }, @@ -7734,7 +7953,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 27, + "file_index": 30, "line": 10 } } @@ -7749,7 +7968,7 @@ "pinned": false }, "source": { - "file_index": 27, + "file_index": 30, "line": 9 } }, @@ -7776,7 +7995,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 28, + "file_index": 31, "line": 3 } } @@ -7791,7 +8010,7 @@ "pinned": false }, "source": { - "file_index": 28, + "file_index": 31, "line": 2 } }, @@ -7842,7 +8061,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 4 } }, @@ -7859,7 +8078,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 8 } } @@ -7892,7 +8111,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 12 } } @@ -7926,7 +8145,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 4 } } @@ -7940,7 +8159,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 2 } }, @@ -7990,7 +8209,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 4 } } @@ -8005,7 +8224,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 4 } }, @@ -8035,7 +8254,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 18 } }, @@ -8052,7 +8271,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 22 } } @@ -8085,7 +8304,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 26 } } @@ -8119,7 +8338,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 18 } } @@ -8133,7 +8352,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 16 } }, @@ -8183,7 +8402,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 18 } } @@ -8198,7 +8417,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 18 } }, @@ -8225,7 +8444,7 @@ "alignment": 8, "is_base": true, "source": { - "file_index": 29, + "file_index": 32, "line": 37 } }, @@ -8242,7 +8461,7 @@ "alignment": 8, "is_base": true, "source": { - "file_index": 29, + "file_index": 32, "line": 39 } } @@ -8276,7 +8495,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 12 } }, @@ -8308,7 +8527,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 26 } }, @@ -8340,7 +8559,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 18 } }, @@ -8371,7 +8590,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 43 } } @@ -8405,7 +8624,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 32 } }, @@ -8436,7 +8655,7 @@ }, "calling_convention": "system", "source": { - "file_index": 29, + "file_index": 32, "line": 33 } } @@ -8450,7 +8669,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 30 } }, @@ -8500,7 +8719,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 32 } }, @@ -8540,7 +8759,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 29, + "file_index": 32, "line": 33 } } @@ -8555,7 +8774,7 @@ "pinned": false }, "source": { - "file_index": 29, + "file_index": 32, "line": 32 } }, @@ -8585,7 +8804,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 30, + "file_index": 33, "line": 9 } } @@ -8600,7 +8819,7 @@ "pinned": false }, "source": { - "file_index": 30, + "file_index": 33, "line": 8 } }, @@ -8636,7 +8855,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 59 } }, @@ -8663,7 +8882,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 60 } } @@ -8678,7 +8897,7 @@ "pinned": false }, "source": { - "file_index": 31, + "file_index": 34, "line": 58 } }, @@ -8703,7 +8922,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 16 } }, @@ -8733,7 +8952,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 33 } }, @@ -8753,7 +8972,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 34 } } @@ -8768,7 +8987,7 @@ "pinned": false }, "source": { - "file_index": 31, + "file_index": 34, "line": 32 } }, @@ -8801,7 +9020,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 25 } }, @@ -8834,7 +9053,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 42 } }, @@ -8857,7 +9076,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 43 } }, @@ -8884,7 +9103,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 44 } }, @@ -8904,7 +9123,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 45 } }, @@ -8924,7 +9143,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 31, + "file_index": 34, "line": 46 } } @@ -8939,7 +9158,7 @@ "pinned": false }, "source": { - "file_index": 31, + "file_index": 34, "line": 41 } }, @@ -8961,7 +9180,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 9 } }, @@ -8986,7 +9205,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 13 } }, @@ -9017,7 +9236,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 28 } }, @@ -9045,7 +9264,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 19 } }, @@ -9067,7 +9286,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 6 } }, @@ -9096,7 +9315,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 54 } }, @@ -9124,7 +9343,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 51 } }, @@ -9152,7 +9371,7 @@ } }, "source": { - "file_index": 31, + "file_index": 34, "line": 22 } }, @@ -9287,7 +9506,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 32, + "file_index": 35, "line": 8 } }, @@ -9304,7 +9523,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 32, + "file_index": 35, "line": 13 } } @@ -9326,7 +9545,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 32, + "file_index": 35, "line": 20 } } @@ -9349,7 +9568,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 32, + "file_index": 35, "line": 8 } } @@ -9363,7 +9582,7 @@ "pinned": false }, "source": { - "file_index": 32, + "file_index": 35, "line": 5 } }, @@ -9403,7 +9622,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 32, + "file_index": 35, "line": 8 } } @@ -9418,7 +9637,7 @@ "pinned": false }, "source": { - "file_index": 32, + "file_index": 35, "line": 8 } }, @@ -9448,7 +9667,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 13 } }, @@ -9465,7 +9684,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 23 } } @@ -9492,7 +9711,7 @@ }, "calling_convention": "system", "source": { - "file_index": 33, + "file_index": 36, "line": 13 } }, @@ -9512,7 +9731,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 33, + "file_index": 36, "line": 11 } }, @@ -9543,7 +9762,7 @@ }, "calling_convention": "system", "source": { - "file_index": 33, + "file_index": 36, "line": 16 } }, @@ -9566,7 +9785,7 @@ }, "calling_convention": "system", "source": { - "file_index": 33, + "file_index": 36, "line": 19 } } @@ -9580,7 +9799,7 @@ "pinned": false }, "source": { - "file_index": 33, + "file_index": 36, "line": 9 } }, @@ -9623,7 +9842,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 13 } }, @@ -9653,7 +9872,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 11 } }, @@ -9693,7 +9912,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 16 } }, @@ -9726,7 +9945,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 33, + "file_index": 36, "line": 19 } } @@ -9741,7 +9960,7 @@ "pinned": false }, "source": { - "file_index": 33, + "file_index": 36, "line": 13 } }, @@ -9789,7 +10008,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 39, "line": 10 } } @@ -9804,7 +10023,7 @@ "pinned": false }, "source": { - "file_index": 36, + "file_index": 39, "line": 9 } }, @@ -9834,7 +10053,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 34, + "file_index": 37, "line": 8 } }, @@ -9851,7 +10070,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 34, + "file_index": 37, "line": 9 } }, @@ -9868,7 +10087,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 34, + "file_index": 37, "line": 10 } } @@ -9883,7 +10102,7 @@ "pinned": false }, "source": { - "file_index": 34, + "file_index": 37, "line": 7 } }, @@ -9910,7 +10129,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 35, + "file_index": 38, "line": 6 } } @@ -9925,7 +10144,7 @@ "pinned": false }, "source": { - "file_index": 35, + "file_index": 38, "line": 5 } }, @@ -9952,7 +10171,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 37, + "file_index": 40, "line": 4 } }, @@ -9969,7 +10188,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 37, + "file_index": 40, "line": 5 } } @@ -9984,7 +10203,7 @@ "pinned": false }, "source": { - "file_index": 37, + "file_index": 40, "line": 3 } }, @@ -10021,7 +10240,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 38, + "file_index": 41, "line": 14 } }, @@ -10041,7 +10260,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 38, + "file_index": 41, "line": 15 } } @@ -10056,7 +10275,7 @@ "pinned": false }, "source": { - "file_index": 38, + "file_index": 41, "line": 13 } }, @@ -10083,7 +10302,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 39, + "file_index": 42, "line": 10 } }, @@ -10100,7 +10319,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 39, + "file_index": 42, "line": 13 } }, @@ -10121,7 +10340,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 39, + "file_index": 42, "line": 6 } } @@ -10136,7 +10355,7 @@ "pinned": false }, "source": { - "file_index": 39, + "file_index": 42, "line": 9 } }, @@ -10167,7 +10386,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 39, + "file_index": 42, "line": 1 } } @@ -10182,7 +10401,7 @@ "pinned": false }, "source": { - "file_index": 39, + "file_index": 42, "line": 4 } } @@ -10539,6 +10758,26 @@ "line": 1 } }, + "doc_self_links": { + "doc": " Self-link testing module. Its own doc links [`Container`] — the module's\n doc block must keep its links separate from its first item's, since the\n module's source location is a proxy borrowed from that item.", + "doc_links": [ + { + "text": "Container", + "target_kind": "item", + "path": "doc_self_links::Container" + } + ], + "items": [ + "doc_self_links::Container" + ], + "submodules": {}, + "functions": [], + "splices": [], + "source": { + "file_index": 12, + "line": 5 + } + }, "enums": { "doc": null, "items": [ @@ -10548,7 +10787,7 @@ "functions": [], "splices": [], "source": { - "file_index": 12, + "file_index": 13, "line": 1 } }, @@ -10562,7 +10801,7 @@ "functions": [], "splices": [], "source": { - "file_index": 13, + "file_index": 14, "line": 1 } }, @@ -10579,7 +10818,7 @@ "functions": [], "splices": [], "source": { - "file_index": 14, + "file_index": 15, "line": 1 } }, @@ -10605,7 +10844,7 @@ }, "calling_convention": "system", "source": { - "file_index": 15, + "file_index": 16, "line": 34 } } @@ -10673,7 +10912,7 @@ } ], "source": { - "file_index": 15, + "file_index": 16, "line": 1 } }, @@ -10687,7 +10926,7 @@ "functions": [], "splices": [], "source": { - "file_index": 16, + "file_index": 17, "line": 1 } }, @@ -10710,7 +10949,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 17, + "file_index": 18, "line": 13 } }, @@ -10738,14 +10977,14 @@ }, "calling_convention": "system", "source": { - "file_index": 17, + "file_index": 18, "line": 17 } } ], "splices": [], "source": { - "file_index": 17, + "file_index": 18, "line": 1 } }, @@ -10768,7 +11007,7 @@ "functions": [], "splices": [], "source": { - "file_index": 18, + "file_index": 19, "line": 1 } }, @@ -10785,7 +11024,7 @@ "functions": [], "splices": [], "source": { - "file_index": 19, + "file_index": 20, "line": 1 } }, @@ -10801,7 +11040,7 @@ "functions": [], "splices": [], "source": { - "file_index": 20, + "file_index": 21, "line": 1 } }, @@ -10815,7 +11054,7 @@ "functions": [], "splices": [], "source": { - "file_index": 21, + "file_index": 22, "line": 1 } }, @@ -10835,7 +11074,7 @@ "functions": [], "splices": [], "source": { - "file_index": 22, + "file_index": 23, "line": 1 } }, @@ -10848,7 +11087,7 @@ "functions": [], "splices": [], "source": { - "file_index": 23, + "file_index": 24, "line": 1 } }, @@ -10864,10 +11103,53 @@ "functions": [], "splices": [], "source": { - "file_index": 24, + "file_index": 25, + "line": 1 + } + }, + "qualified_links_consumer": { + "doc": null, + "items": [ + "qualified_links_consumer::Consumer" + ], + "submodules": {}, + "functions": [], + "splices": [], + "source": { + "file_index": 26, "line": 1 } }, + "qualified_links_provider": { + "doc": null, + "items": [ + "qualified_links_provider::shared_extern" + ], + "submodules": {}, + "functions": [ + { + "visibility": "public", + "name": "shared_function", + "doc": " A module with a freestanding function.", + "body": { + "type": "address", + "address": 256 + }, + "arguments": [], + "return_type": null, + "calling_convention": "system", + "source": { + "file_index": 27, + "line": 3 + } + } + ], + "splices": [], + "source": { + "file_index": 27, + "line": 3 + } + }, "reexport": { "doc": null, "items": [ @@ -10887,7 +11169,7 @@ "functions": [], "splices": [], "source": { - "file_index": 25, + "file_index": 28, "line": 1 } }, @@ -10900,7 +11182,7 @@ "functions": [], "splices": [], "source": { - "file_index": 26, + "file_index": 29, "line": 1 } }, @@ -10924,7 +11206,7 @@ "functions": [], "splices": [], "source": { - "file_index": 27, + "file_index": 30, "line": 1 } }, @@ -10937,7 +11219,7 @@ "functions": [], "splices": [], "source": { - "file_index": 28, + "file_index": 31, "line": 1 } }, @@ -10955,7 +11237,7 @@ "functions": [], "splices": [], "source": { - "file_index": 29, + "file_index": 32, "line": 1 } }, @@ -10968,7 +11250,7 @@ "functions": [], "splices": [], "source": { - "file_index": 30, + "file_index": 33, "line": 1 } }, @@ -10993,7 +11275,7 @@ "functions": [], "splices": [], "source": { - "file_index": 31, + "file_index": 34, "line": 1 } }, @@ -11007,7 +11289,7 @@ "functions": [], "splices": [], "source": { - "file_index": 32, + "file_index": 35, "line": 3 } }, @@ -11021,7 +11303,7 @@ "functions": [], "splices": [], "source": { - "file_index": 33, + "file_index": 36, "line": 8 } }, @@ -11040,7 +11322,7 @@ "functions": [], "splices": [], "source": { - "file_index": 34, + "file_index": 37, "line": 1 } }, @@ -11057,7 +11339,7 @@ "functions": [], "splices": [], "source": { - "file_index": 35, + "file_index": 38, "line": 1 } } @@ -11075,7 +11357,7 @@ "functions": [], "splices": [], "source": { - "file_index": 37, + "file_index": 40, "line": 1 } } @@ -11094,7 +11376,7 @@ } ], "source": { - "file_index": 36, + "file_index": 39, "line": 7 } }, @@ -11107,7 +11389,7 @@ "functions": [], "splices": [], "source": { - "file_index": 38, + "file_index": 41, "line": 1 } }, @@ -11121,7 +11403,7 @@ "functions": [], "splices": [], "source": { - "file_index": 39, + "file_index": 42, "line": 1 } } @@ -11139,6 +11421,7 @@ "definition_body.pyxis", "diamond_inheritance.pyxis", "doc_comments.pyxis", + "doc_self_links.pyxis", "enums.pyxis", "extern_bindings.pyxis", "extern_values.pyxis", @@ -11152,6 +11435,8 @@ "multiple_levels.pyxis", "nested_items.pyxis", "pinned.pyxis", + "qualified_links_consumer.pyxis", + "qualified_links_provider.pyxis", "reexport.pyxis", "reexport_consumer.pyxis", "self_referential_generics.pyxis", diff --git a/codegen_tests/output/rust/consts.rs b/codegen_tests/output/rust/consts.rs index 3c1d68cf..266c51ad 100644 --- a/codegen_tests/output/rust/consts.rs +++ b/codegen_tests/output/rust/consts.rs @@ -1,8 +1,7 @@ #![cfg_attr(any(), rustfmt::skip)] -#[allow(unused_imports)] -use Player_Inventory as Inventory; crate::__bitflags! { - #[doc = " The permissive baseline is [`DEFAULT_MASK`](AccessFlags::DEFAULT_MASK)."] + #[doc = + " The permissive baseline is [`DEFAULT_MASK`](crate::consts::AccessFlags::DEFAULT_MASK)."] pub struct AccessFlags : u32 { const READ = 1usize as _; const WRITE = 2usize as _; } } fn _AccessFlags_size_check() { @@ -16,7 +15,7 @@ impl AccessFlags { } #[repr(u8)] #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -/// Defaults to [`DEFAULT`](Color::DEFAULT). +/// Defaults to [`DEFAULT`](crate::consts::Color::DEFAULT). pub enum Color { Red = 0isize as _, Green = 1isize as _, @@ -114,9 +113,9 @@ pub const IDENTITY: crate::math::Matrix4 = crate::math::Matrix4 { pub const MAX_HEALTH: i32 = 100; #[repr(C, align(4))] pub struct Player { - /// New players begin with [`STARTING_GOLD`](Player::STARTING_GOLD) gold, - /// spawn at x=[`SPAWN_X`](Player::SPAWN_X), and can hold up to - /// [`MAX_SLOTS`](Player_Inventory::MAX_SLOTS) items. + /// New players begin with [`STARTING_GOLD`](crate::consts::Player::STARTING_GOLD) gold, + /// spawn at x=[`SPAWN_X`](crate::consts::Player::SPAWN_X), and can hold up to + /// [`MAX_SLOTS`](crate::consts::Player_Inventory::MAX_SLOTS) items. pub health: i32, } fn _Player_size_check() { diff --git a/codegen_tests/output/rust/doc_self_links.rs b/codegen_tests/output/rust/doc_self_links.rs new file mode 100644 index 00000000..3e60ac39 --- /dev/null +++ b/codegen_tests/output/rust/doc_self_links.rs @@ -0,0 +1,64 @@ +#![cfg_attr(any(), rustfmt::skip)] +//! Self-link testing module. Its own doc links [`Container`](crate::doc_self_links::Container) — the module's +//! doc block must keep its links separate from its first item's, since the +//! module's source location is a proxy borrowed from that item. +#[repr(C, align(4))] +/// Test `Self::` links in type docs. +/// +/// Link to a field as [`Self::field`](crate::doc_self_links::Container::field), to a method as +/// [`Self::method`](crate::doc_self_links::Container::method), to a nested type as +/// [`Self::Nested`](crate::doc_self_links::Container_Nested), and to a nested type's member as +/// [`Self::Nested::nested_field`](crate::doc_self_links::Container_Nested::nested_field). +pub struct Container { + /// A field. + pub field: u32, +} +fn _Container_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x4], Container>([0u8; 0x4]); + } + unreachable!() +} +impl Container { + /// A method. + pub unsafe fn method(&self) { + unsafe { + let f: unsafe extern "system" fn(this: *const Self) = ::std::mem::transmute( + 0x10 as usize, + ); + f(self as *const Self as _) + } + } +} +impl std::convert::AsRef for Container { + fn as_ref(&self) -> &Container { + self + } +} +impl std::convert::AsMut for Container { + fn as_mut(&mut self) -> &mut Container { + self + } +} +#[repr(C, align(2))] +/// A nested type. Its field is [`Self::nested_field`](crate::doc_self_links::Container_Nested::nested_field). +pub struct Container_Nested { + pub nested_field: u16, +} +fn _Container_Nested_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x2], Container_Nested>([0u8; 0x2]); + } + unreachable!() +} +impl Container_Nested {} +impl std::convert::AsRef for Container_Nested { + fn as_ref(&self) -> &Container_Nested { + self + } +} +impl std::convert::AsMut for Container_Nested { + fn as_mut(&mut self) -> &mut Container_Nested { + self + } +} diff --git a/codegen_tests/output/rust/extern_values.rs b/codegen_tests/output/rust/extern_values.rs index 0c84a7bd..6e6c30f8 100644 --- a/codegen_tests/output/rust/extern_values.rs +++ b/codegen_tests/output/rust/extern_values.rs @@ -59,7 +59,7 @@ impl RenderMode { unsafe { &mut *(0x4000 as *mut *mut crate::extern_values::RenderMode) } } } -/// The engine singleton, of type [`Engine`]. Advances with [`get_g_frame_count`]. +/// The engine singleton, of type [`Engine`](crate::extern_values::Engine). Advances with [`g_frame_count`](get_g_frame_count). pub unsafe fn get_g_engine() -> &'static mut *mut crate::extern_values::Engine { unsafe { &mut *(0x2000 as *mut *mut crate::extern_values::Engine) } } diff --git a/codegen_tests/output/rust/lib.rs b/codegen_tests/output/rust/lib.rs index 04302d13..26431c84 100644 --- a/codegen_tests/output/rust/lib.rs +++ b/codegen_tests/output/rust/lib.rs @@ -5,7 +5,8 @@ non_upper_case_globals, clippy::missing_safety_doc, clippy::unnecessary_cast, - clippy::module_inception + clippy::module_inception, + rustdoc::redundant_explicit_links )] #![cfg_attr(any(), rustfmt::skip)] #[macro_export] @@ -106,6 +107,7 @@ pub mod consts; pub mod definition_body; pub mod diamond_inheritance; pub mod doc_comments; +pub mod doc_self_links; pub mod enums; pub mod extern_bindings; pub mod extern_values; @@ -119,6 +121,8 @@ pub mod min_size; pub mod multiple_levels; pub mod nested_items; pub mod pinned; +pub mod qualified_links_consumer; +pub mod qualified_links_provider; pub mod reexport; pub mod reexport_consumer; pub mod self_referential_generics; diff --git a/codegen_tests/output/rust/nested_items.rs b/codegen_tests/output/rust/nested_items.rs index a4f5a0c3..9cd1fd41 100644 --- a/codegen_tests/output/rust/nested_items.rs +++ b/codegen_tests/output/rust/nested_items.rs @@ -1,18 +1,10 @@ #![cfg_attr(any(), rustfmt::skip)] -#[allow(unused_imports)] -use Outer_InnerAlias as InnerAlias; -#[allow(unused_imports)] -use Outer_InnerEnum as InnerEnum; -#[allow(unused_imports)] -use Outer_InnerFlags as InnerFlags; -#[allow(unused_imports)] -use Outer_InnerType as InnerType; #[repr(C, align(4))] /// A type with nested declarations. /// -/// See [`Outer_InnerEnum`], [`Outer_InnerType`], [`Outer_InnerFlags`], and [`Outer_InnerAlias`]. +/// See [`InnerEnum`](crate::nested_items::Outer_InnerEnum), [`InnerType`](crate::nested_items::Outer_InnerType), [`InnerFlags`](crate::nested_items::Outer_InnerFlags), and [`InnerAlias`](crate::nested_items::Outer_InnerAlias). /// -/// You can also qualify them: [`Outer_InnerEnum`], [`Outer_InnerType`]. +/// You can also qualify them: [`Outer::InnerEnum`](crate::nested_items::Outer_InnerEnum), [`Outer::InnerType`](crate::nested_items::Outer_InnerType). pub struct Outer { pub field: u32, } @@ -33,13 +25,13 @@ impl std::convert::AsMut for Outer { self } } -/// A type alias nested inside [`Outer`]. +/// A type alias nested inside [`Outer`](crate::nested_items::Outer). pub type Outer_InnerAlias = u32; #[repr(u8)] #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -/// An enum nested inside [`Outer`]. +/// An enum nested inside [`Outer`](crate::nested_items::Outer). /// -/// Variants: [`Outer_InnerEnum::A`], [`Outer_InnerEnum::B`], [`Outer_InnerEnum::C`]. +/// Variants: [`InnerEnum::A`](crate::nested_items::Outer_InnerEnum::A), [`InnerEnum::B`](crate::nested_items::Outer_InnerEnum::B), [`InnerEnum::C`](crate::nested_items::Outer_InnerEnum::C). pub enum Outer_InnerEnum { A = 0isize as _, B = 1isize as _, @@ -52,9 +44,11 @@ fn _Outer_InnerEnum_size_check() { unreachable!() } crate::__bitflags! { - #[doc = " Bitflags nested inside [`Outer`]."] #[doc = ""] #[doc = - " Members: [`Outer_InnerFlags::FLAG_A`], [`Outer_InnerFlags::FLAG_B`]."] pub struct - Outer_InnerFlags : u32 { const FLAG_A = 1usize as _; const FLAG_B = 2usize as _; } + #[doc = " Bitflags nested inside [`Outer`](crate::nested_items::Outer)."] #[doc = ""] + #[doc = + " Members: [`InnerFlags::FLAG_A`](crate::nested_items::Outer_InnerFlags::FLAG_A), [`InnerFlags::FLAG_B`](crate::nested_items::Outer_InnerFlags::FLAG_B)."] + pub struct Outer_InnerFlags : u32 { const FLAG_A = 1usize as _; const FLAG_B = 2usize + as _; } } fn _Outer_InnerFlags_size_check() { unsafe { @@ -63,9 +57,9 @@ fn _Outer_InnerFlags_size_check() { unreachable!() } #[repr(C, align(2))] -/// A type nested inside [`Outer`]. +/// A type nested inside [`Outer`](crate::nested_items::Outer). /// -/// Its field is [`Outer_InnerType::inner_field`]. +/// Its field is [`InnerType::inner_field`](crate::nested_items::Outer_InnerType::inner_field). pub struct Outer_InnerType { pub inner_field: u16, } diff --git a/codegen_tests/output/rust/qualified_links_consumer.rs b/codegen_tests/output/rust/qualified_links_consumer.rs new file mode 100644 index 00000000..9d02800d --- /dev/null +++ b/codegen_tests/output/rust/qualified_links_consumer.rs @@ -0,0 +1,26 @@ +#![cfg_attr(any(), rustfmt::skip)] +#[repr(C, align(1))] +/// Test module-qualified doc-links to functions and extern values. +/// +/// See [`shared_function`](crate::qualified_links_provider::shared_function) and +/// [`shared_extern`](crate::qualified_links_provider::get_shared_extern). +pub struct Consumer { + pub _marker: u8, +} +fn _Consumer_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x1], Consumer>([0u8; 0x1]); + } + unreachable!() +} +impl Consumer {} +impl std::convert::AsRef for Consumer { + fn as_ref(&self) -> &Consumer { + self + } +} +impl std::convert::AsMut for Consumer { + fn as_mut(&mut self) -> &mut Consumer { + self + } +} diff --git a/codegen_tests/output/rust/qualified_links_provider.rs b/codegen_tests/output/rust/qualified_links_provider.rs new file mode 100644 index 00000000..ac0d6222 --- /dev/null +++ b/codegen_tests/output/rust/qualified_links_provider.rs @@ -0,0 +1,12 @@ +#![cfg_attr(any(), rustfmt::skip)] +/// An extern value. +pub unsafe fn get_shared_extern() -> &'static mut *mut u32 { + unsafe { &mut *(0x200 as *mut *mut u32) } +} +/// A module with a freestanding function. +pub unsafe fn shared_function() { + unsafe { + let f: unsafe extern "system" fn() = ::std::mem::transmute(0x100 as usize); + f() + } +} diff --git a/docs/cpp_backend.md b/docs/cpp_backend.md index 0e0b615b..e210cf89 100644 --- a/docs/cpp_backend.md +++ b/docs/cpp_backend.md @@ -270,6 +270,18 @@ with `CppBackendError::LayoutCycle` if it finds one. Break the cycle by introducing a pointer indirection or moving a type into a separate module. +## Doc comments and doxygen links + +Doc comments are emitted as `///` comments, which doxygen picks up natively. Rustdoc-style intra-doc links are rewritten from their resolved semantic targets into doxygen-markdown references: + +```cpp +/// Link to a field as [`Self::field`](@ref doc_self_links::Container::field). +``` + +Rendering follows the C++ emission rules rather than the written path: module segments become namespaces, nested types stay genuinely nested (`ns::Outer::Inner`), extern values map to their `get_()` accessor, and nested constants/extern values under an enum or bitflags parent — which the emitter flattens to module scope, since those have no struct body — become `ns::Parent_NAME` / `ns::Parent_get_name`. Links to predefined primitives and out-of-tree extern types have no documented entity to reference and are flattened to their bare label instead of emitting a dead link. + +`test.py` verifies the emitted corpus with doxygen (any warning, including an unresolvable `@ref`, fails the suite). The check is skipped with a warning when doxygen isn't installed; `shell.nix` provides it. + ## Building on Linux The emitted `CMakeLists.txt` is normative — no toolchain assumptions. diff --git a/docs/language.md b/docs/language.md index 4713e60d..d7c67f14 100644 --- a/docs/language.md +++ b/docs/language.md @@ -900,9 +900,11 @@ pub type Outer { Link forms: - `[`Item`]` - shortcut link to an item in scope. - `[`Item::Field`]` - link to a field, variant, or method. +- `[`Self::member`]` - link to a member of the enclosing type. Inside a nested item, `Self` refers to the nested item; in a nested constant or extern value's doc, it refers to the parent type. +- `[`module::function`]` - module-qualified link to a freestanding function or extern value. - `[`display text`](Path)` - inline link with custom display text. -The Rust backend rewrites links to fully-qualified `crate::` paths at emission time, adding cross-module imports as needed. The JSON backend resolves links to absolute paths and attaches them as structured `doc_links` data so the viewer can render clickable references. +Every link is resolved once during semantic analysis (an unresolvable link is a build error), and each backend renders the resolved target in its own idiom: the Rust backend rewrites destinations to fully-qualified `crate::` paths, the C++ backend rewrites them to doxygen `[label](@ref ns::Target)` references, and the JSON backend attaches them as structured `doc_links` data so the viewer can render clickable references. ### Unicode diff --git a/docs/rust_backend.md b/docs/rust_backend.md index ff4ebc16..1ba948ab 100644 --- a/docs/rust_backend.md +++ b/docs/rust_backend.md @@ -124,15 +124,17 @@ Rust permits multiple `impl Foo` blocks, so the user's epilogue can host its own ## Doc link rewriting -Rustdoc-style intra-doc links (`[`Item`]`, `[`Item::Field`]`, `[`display text`](Path)`) are rewritten to fully-qualified `crate::` paths at emission time. +Rustdoc-style intra-doc links (`[`Item`]`, `[`Item::Field`]`, `[`Self::member`]`, `[`display text`](Path)`) are rewritten at emission time by rendering each link's *resolved target* as an absolute Rust path. The process: -1. The semantic resolver collects all doc links in each module and resolves them to target item paths. -2. Cross-module links trigger imports - the target's defining module is added to the current module's `use` list. -3. Nested items (types declared inside other types) are flattened in Rust (`Outer::Inner` → `Outer_Inner`), so doc links referencing nested items are rewritten to their flattened names. +1. Semantic analysis resolves every doc link once, crate-wide, and stores the results per doc block (`doc_links::resolve_all`). Unresolvable links fail the build there. +2. The backend renders each resolved target as an absolute `crate::` path: nested items are flattened (`Outer::Inner` → `crate::module::Outer_Inner`), extern values become their `get_` accessor path, and members append to their owner's rendered path. Links to predefined types (`u32`, ...) are left alone — rustdoc resolves primitives natively. +3. An inline link keeps its label and has its destination replaced; a code shortcut (`` [`Inner`] ``) becomes an inline link (`` [`Inner`](crate::module::Outer_Inner) ``) so the written label survives. 4. The rewritten links are emitted in the `#[doc = "..."]` attribute strings. +Because every destination is absolute, no doc-driven `use` imports or aliases are emitted, and rewriting never depends on how the link was written — only on what it resolved to. The generated root allows `rustdoc::redundant_explicit_links`, since the uniform rewrite is "redundant" for the subset of links rustdoc could have resolved from the label alone. + The link detection is shared between the compiler and the LSP via `scan_links`, so hover previews and generated docs see the same link resolution. ## Extern type bindings diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..546ab6bd --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +# Dev-shell extras for pyxis — tools `python test.py` uses beyond the Rust +# toolchain (which contributors typically manage via rustup). +# +# Currently that's doxygen (plus graphviz for its dot graphs), for the C++ +# doc-link check: test.py runs doxygen over the emitted C++ corpus and fails +# on unresolved `@ref`s. Without doxygen the check is skipped with a warning. +{ + pkgs ? import { }, +}: +pkgs.mkShell { + packages = with pkgs; [ + doxygen + graphviz + ]; +} diff --git a/src/backends/cpp/mod.rs b/src/backends/cpp/mod.rs index d13d82ed..ed309a13 100644 --- a/src/backends/cpp/mod.rs +++ b/src/backends/cpp/mod.rs @@ -672,7 +672,13 @@ fn write_module( let cfg_ctx = crate::parser::cfg::CfgContext { backend: crate::Backend::Cpp, }; - let ctx = render::RenderCtx::new(key, registry, bindings, cfg_ctx); + let ctx = render::RenderCtx::new( + key, + registry, + bindings, + cfg_ctx, + semantic_state.module_doc_links(key), + ); let module_deps = deps::collect_module_deps(key, module, registry, bindings); let splices = extract_cpp_splices(module); let body = render_module_body(key, module, registry, bindings, ctx)?; diff --git a/src/backends/cpp/render.rs b/src/backends/cpp/render.rs index 69c155ba..792f243e 100644 --- a/src/backends/cpp/render.rs +++ b/src/backends/cpp/render.rs @@ -40,6 +40,9 @@ pub struct RenderCtx<'a> { pub registry: &'a TypeRegistry, pub bindings: &'a BTreeMap, pub cfg_ctx: crate::parser::cfg::CfgContext, + /// Resolved intra-doc links of the module being rendered, for rewriting + /// doc-comment links into doxygen `@ref`s. + pub doc_links: &'a crate::semantic::doc_links::ModuleDocLinks, /// Member names of the class currently being rendered, if any. A /// same-module type reference whose leaf is in this set collides with a /// member and must be emitted qualified (see [`render_path`]). `None` @@ -53,12 +56,14 @@ impl<'a> RenderCtx<'a> { registry: &'a TypeRegistry, bindings: &'a BTreeMap, cfg_ctx: crate::parser::cfg::CfgContext, + doc_links: &'a crate::semantic::doc_links::ModuleDocLinks, ) -> Self { Self { module_path, registry, bindings, cfg_ctx, + doc_links, shadowed_members: None, } } @@ -134,9 +139,11 @@ pub fn render_item(item: &ItemDefinition, ctx: RenderCtx) -> Result { - let (mut decl, mut post_cpp) = render_enum(&name, ed, resolved.size, ctx)?; + let (mut decl, mut post_cpp) = + render_enum(&name, ed, resolved.size, ctx, &item.location)?; // Enums have no struct body for static members, so nested value items // are flattened to module scope: `constexpr` consts in the header, // extern-value getters declared in the header and defined in the `.cpp`. @@ -148,7 +155,8 @@ pub fn render_item(item: &ItemDefinition, ctx: RenderCtx) -> Result { - let (mut decl, mut post_cpp) = render_bitflags(&name, bd, resolved.size, ctx)?; + let (mut decl, mut post_cpp) = + render_bitflags(&name, bd, resolved.size, ctx, &item.location)?; render_nested_values_cpp_flat(&mut decl, &mut post_cpp, ctx, &item.path, &name)?; RenderedItem { decl, @@ -157,14 +165,14 @@ pub fn render_item(item: &ItemDefinition, ctx: RenderCtx) -> Result RenderedItem { - decl: render_type_alias(&name, ta, ctx, &item.type_parameters)?, + decl: render_type_alias(&name, ta, ctx, &item.type_parameters, &item.location)?, post_header: String::new(), post_cpp: String::new(), }, - ItemDefinitionInner::Constant(cd) => render_const(&name, cd, ctx)?, + ItemDefinitionInner::Constant(cd) => render_const(&name, cd, ctx, &item.location)?, ItemDefinitionInner::ExternValue(ev) => { let mut decl = String::new(); - render_doc(&mut decl, &ev.doc, 0)?; + render_doc(&mut decl, &ev.doc, 0, ctx, &item.location)?; decl.push_str(&render_extern_value_decl(&name, ev, ctx)?); let post_cpp = render_extern_value_definition(&name, ev, ctx)?; RenderedItem { @@ -189,6 +197,7 @@ fn template_clause(type_parameters: &[String]) -> String { format!("template <{params}>\n") } +#[allow(clippy::too_many_arguments)] fn render_struct( name: &str, td: &TypeDefinition, @@ -197,6 +206,7 @@ fn render_struct( ctx: RenderCtx, visibility: Visibility, type_parameters: &[String], + location: &ItemLocation, ) -> Result { let name = &*cpp_ident(name); let is_generic = !type_parameters.is_empty(); @@ -221,7 +231,7 @@ fn render_struct( let ctx = ctx.with_shadowed_members(&shadowed_members); let mut out = String::new(); - render_doc(&mut out, &td.doc, 0)?; + render_doc(&mut out, &td.doc, 0, ctx, location)?; if td.packed { writeln!(out, "#pragma pack(push, 1)")?; } @@ -337,7 +347,7 @@ fn render_struct( let nested_name = cpp_ident(&nested_name); match &nested_resolved.inner { ItemDefinitionInner::Type(nested_td) => { - render_doc(&mut body, &nested_td.doc, 1)?; + render_doc(&mut body, &nested_td.doc, 1, ctx, &nested_item.location)?; writeln!(body, " struct {nested_name} {{")?; let nested_had_fields = !nested_td.regions.is_empty(); for region in &nested_td.regions { @@ -371,7 +381,13 @@ fn render_struct( .map(|s| s.as_str().to_string()) .unwrap_or_default(); let nested_const_name = cpp_ident(&nested_const_name); - render_doc(&mut body, &nested_cd.doc, 2)?; + render_doc( + &mut body, + &nested_cd.doc, + 2, + ctx, + &nested_nested_item.location, + )?; let bf_type = render_type(&nested_cd.type_, ctx)?; let value_str = format_const_value(&nested_cd.value, &nested_cd.type_); @@ -384,7 +400,7 @@ fn render_struct( writeln!(body, " }};")?; } ItemDefinitionInner::Enum(nested_ed) => { - render_doc(&mut body, &nested_ed.doc, 1)?; + render_doc(&mut body, &nested_ed.doc, 1, ctx, &nested_item.location)?; writeln!( body, " enum class {nested_name} : {} {{", @@ -401,7 +417,7 @@ fn render_struct( writeln!(body, " }};")?; } ItemDefinitionInner::Bitflags(nested_bd) => { - render_doc(&mut body, &nested_bd.doc, 1)?; + render_doc(&mut body, &nested_bd.doc, 1, ctx, &nested_item.location)?; writeln!(body, " struct {nested_name} {{")?; let bf_type = render_type(&nested_bd.type_, ctx)?; for flag in &nested_bd.flags { @@ -415,7 +431,7 @@ fn render_struct( writeln!(body, " }};")?; } ItemDefinitionInner::TypeAlias(nested_ta) => { - render_doc(&mut body, &nested_ta.doc, 1)?; + render_doc(&mut body, &nested_ta.doc, 1, ctx, &nested_item.location)?; writeln!( body, " using {nested_name} = {};", @@ -423,7 +439,7 @@ fn render_struct( )?; } ItemDefinitionInner::Constant(nested_cd) => { - render_doc(&mut body, &nested_cd.doc, 1)?; + render_doc(&mut body, &nested_cd.doc, 1, ctx, &nested_item.location)?; let bf_type = render_type(&nested_cd.type_, ctx)?; let value_str = format_const_value(&nested_cd.value, &nested_cd.type_); // For scalar/POD types, use `static constexpr` with @@ -456,7 +472,7 @@ fn render_struct( // Declared here; defined out-of-class below (in the // `.cpp` for non-templates, like the singleton // accessor and member functions). - render_doc(&mut body, &nested_ev.doc, 1)?; + render_doc(&mut body, &nested_ev.doc, 1, ctx, &nested_item.location)?; let ev_type = render_type(&nested_ev.type_, ctx)?; writeln!(body, " static {ev_type}& get_{nested_name}();")?; } @@ -639,7 +655,7 @@ fn render_method_signature(out: &mut String, func: &Function, ctx: RenderCtx) -> if func.name.starts_with("_vfunc_") { return Ok(()); } - render_doc(out, &func.doc, 1)?; + render_doc(out, &func.doc, 1, ctx, &func.location)?; let (return_text, sig_args_text, const_qual) = method_sig_parts(func, ctx)?; let static_kw = if func_has_self(func) { "" } else { "static " }; // Method-level template parameters (e.g. `Y` in @@ -852,7 +868,7 @@ fn render_field_indented( indent: usize, ) -> Result<()> { let pad = " ".repeat(indent * 4); - render_doc(out, ®ion.doc, indent)?; + render_doc(out, ®ion.doc, indent, ctx, ®ion.location)?; let Some(field_name) = region.name.as_deref() else { // Should not happen post-resolution, but be defensive. writeln!(out, "{pad}// ")?; @@ -941,10 +957,11 @@ fn render_enum( ed: &EnumDefinition, size: usize, ctx: RenderCtx, + location: &ItemLocation, ) -> Result<(String, String)> { let name = &*cpp_ident(name); let mut out = String::new(); - render_doc(&mut out, &ed.doc, 0)?; + render_doc(&mut out, &ed.doc, 0, ctx, location)?; let underlying = render_type(&ed.type_, ctx)?; writeln!(out, "enum class {name} : {underlying} {{")?; for variant in &ed.variants { @@ -978,10 +995,11 @@ fn render_bitflags( bd: &BitflagsDefinition, size: usize, ctx: RenderCtx, + location: &ItemLocation, ) -> Result<(String, String)> { let name = &*cpp_ident(name); let mut out = String::new(); - render_doc(&mut out, &bd.doc, 0)?; + render_doc(&mut out, &bd.doc, 0, ctx, location)?; let underlying = render_type(&bd.type_, ctx)?; writeln!(out, "enum class {name} : {underlying} {{")?; for flag in &bd.flags { @@ -1049,10 +1067,11 @@ fn render_type_alias( ta: &TypeAliasDefinition, ctx: RenderCtx, type_parameters: &[String], + location: &ItemLocation, ) -> Result { let name = &*cpp_ident(name); let mut out = String::new(); - render_doc(&mut out, &ta.doc, 0)?; + render_doc(&mut out, &ta.doc, 0, ctx, location)?; let target = render_type(&ta.target, ctx)?; let template = template_clause(type_parameters); writeln!(out, "{template}using {name} = {target};")?; @@ -1091,7 +1110,7 @@ fn render_nested_values_cpp_flat( } _ => "constexpr", }; - render_doc(decl_out, &cd.doc, 0)?; + render_doc(decl_out, &cd.doc, 0, ctx, &item.location)?; writeln!(decl_out, "{storage} {type_str} {flat_name} = {value_str};")?; } ItemDefinitionInner::ExternValue(ev) => { @@ -1099,7 +1118,7 @@ fn render_nested_values_cpp_flat( // module-level extern-value getters and the singleton accessor. let flat_name = format!("{parent_name}_get_{}", cpp_ident(value_name)); let type_str = render_type(&ev.type_, ctx)?; - render_doc(decl_out, &ev.doc, 0)?; + render_doc(decl_out, &ev.doc, 0, ctx, &item.location)?; writeln!(decl_out, "{type_str}& {flat_name}();")?; writeln!(cpp_out, "{type_str}& {flat_name}() {{")?; writeln!( @@ -1217,10 +1236,15 @@ fn format_const_value(value: &ConstValue, type_: &Type) -> String { } } -fn render_const(name: &str, cd: &SemanticConstDefinition, ctx: RenderCtx) -> Result { +fn render_const( + name: &str, + cd: &SemanticConstDefinition, + ctx: RenderCtx, + location: &ItemLocation, +) -> Result { let name = &*cpp_ident(name); let mut decl = String::new(); - render_doc(&mut decl, &cd.doc, 0)?; + render_doc(&mut decl, &cd.doc, 0, ctx, location)?; let type_str = render_type(&cd.type_, ctx)?; let value_str = format_const_value(&cd.value, &cd.type_); // Use `constexpr` for scalar/POD types, `inline const` for @@ -1246,7 +1270,7 @@ fn render_const(name: &str, cd: &SemanticConstDefinition, ctx: RenderCtx) -> Res /// declaration whose body is supplied by the user's `backend cpp` block. pub fn render_free_function_decl(func: &Function, ctx: RenderCtx) -> Result> { let mut out = String::new(); - render_doc(&mut out, &func.doc, 0)?; + render_doc(&mut out, &func.doc, 0, ctx, &func.location)?; let name = cpp_ident(&func.name); match &func.body { FunctionBody::Address { .. } => { @@ -1353,7 +1377,14 @@ pub fn render_extern_value_definition( )) } -fn render_doc(out: &mut String, doc: &[String], indent_levels: usize) -> Result<()> { +fn render_doc( + out: &mut String, + doc: &[String], + indent_levels: usize, + ctx: RenderCtx, + location: &ItemLocation, +) -> Result<()> { + let links = ctx.doc_links.at(location); let pad = " ".repeat(indent_levels); for line in doc { let trimmed = line.trim(); @@ -1361,12 +1392,152 @@ fn render_doc(out: &mut String, doc: &[String], indent_levels: usize) -> Result< // Blank doc line - emit just `///` with no trailing space. writeln!(out, "{pad}///")?; } else { - writeln!(out, "{pad}/// {trimmed}")?; + let rewritten = rewrite_doc_links(trimmed, links, ctx); + writeln!(out, "{pad}/// {rewritten}")?; } } Ok(()) } +/// Rewrite each resolved intra-doc link in `line` into a doxygen-resolvable +/// form: the markdown destination becomes `@ref ` +/// (`[`field`](Self::field)` → `[`field`](@ref ns::Container::field)`), and a +/// code shortcut becomes an inline link so its written label survives. +/// Doxygen's markdown support resolves `[label](@ref target)` to the target's +/// documentation. +/// +/// Links whose target has no documented C++ entity (predefined primitives, +/// externs bound to out-of-tree types) are flattened to their bare label — +/// leaving the raw path as a markdown destination would render a dead +/// `href="Path::To"` link. +fn rewrite_doc_links( + line: &str, + links: &[crate::semantic::doc_links::ResolvedDocLink], + ctx: RenderCtx, +) -> String { + use crate::semantic::doc_links::{DocLinkSyntax, scan_links}; + if links.is_empty() { + return line.to_string(); + } + let mut result = line.to_string(); + let mut scanned = scan_links(line); + scanned.retain(|l| l.syntax != DocLinkSyntax::PlainShortcut); + for link in scanned.into_iter().rev() { + let Some(resolved) = links.iter().find(|r| r.text == link.path) else { + continue; + }; + let label = &line[link.label_region.0..link.label_region.1]; + let replacement = match doxygen_ref(&resolved.target, ctx) { + Some(target) => format!("[{label}](@ref {target})"), + None => label.to_string(), + }; + result.replace_range(link.link.0..link.link.1, &replacement); + } + result +} + +/// The fully-qualified C++ name of a resolved link target, for a doxygen +/// `@ref` — or `None` when no documented C++ entity exists for it. +/// +/// Module segments are namespaces and nested types stay genuinely nested in +/// C++, so the item path maps segment-for-segment (with identifier escaping). +/// Extern values map to their `get_` accessor. +fn doxygen_ref( + target: &crate::semantic::doc_links::DocLinkTarget, + ctx: RenderCtx, +) -> Option { + use crate::semantic::doc_links::{DocLinkMemberKind, DocLinkTarget}; + + let qualify = |path: &ItemPath| -> Option { + // Predefined items are C++ primitives; externs may be bound to + // out-of-tree types. Neither has documentation to reference. + if let Ok(item) = ctx + .registry + .get(path, &crate::span::ItemLocation::internal()) + { + if item.predefined.is_some() + || matches!(item.category, crate::semantic::types::ItemCategory::Extern) + { + return None; + } + } + let last = path.len().saturating_sub(1); + Some( + path.iter() + .enumerate() + .map(|(i, seg)| { + if i == last { + cpp_ident(seg.as_str()).into_owned() + } else { + cpp_namespace_ident(seg.as_str()).into_owned() + } + }) + .collect::>() + .join("::"), + ) + }; + + match target { + DocLinkTarget::Item(path) => qualify(path), + DocLinkTarget::Member { item, name, kind } => { + let base = qualify(item)?; + // Nested constants/extern values under an enum or bitflags parent + // have no struct body to live in; the emitter flattens them to + // module scope as `Parent_NAME` / `Parent_get_name()` (see + // `render_nested_values_cpp_flat`). Mirror that here. + let parent_is_bodyless = ctx + .registry + .get(item, &crate::span::ItemLocation::internal()) + .ok() + .and_then(|i| i.resolved()) + .is_some_and(|r| { + matches!( + r.inner, + ItemDefinitionInner::Enum(_) | ItemDefinitionInner::Bitflags(_) + ) + }); + let value_member = matches!( + kind, + DocLinkMemberKind::Constant | DocLinkMemberKind::ExternValue + ); + let accessor = |name: &str| match kind { + DocLinkMemberKind::ExternValue => format!("get_{}", cpp_ident(name)), + _ => cpp_ident(name).into_owned(), + }; + Some(if parent_is_bodyless && value_member { + format!("{base}_{}", accessor(name)) + } else { + format!("{base}::{}", accessor(name)) + }) + } + DocLinkTarget::Function { module, name } => { + let ns = module + .iter() + .map(|s| cpp_namespace_ident(s.as_str()).into_owned()) + .collect::>() + .join("::"); + Some(if ns.is_empty() { + cpp_ident(name).into_owned() + } else { + format!("{ns}::{}", cpp_ident(name)) + }) + } + DocLinkTarget::ExternValue { module, name } => { + let ns = module + .iter() + .map(|s| cpp_namespace_ident(s.as_str()).into_owned()) + .collect::>() + .join("::"); + let accessor = format!("get_{}", cpp_ident(name)); + Some(if ns.is_empty() { + accessor + } else { + format!("{ns}::{accessor}") + }) + } + } +} + /// Render a `Type` as a C++ type expression. For arrays the caller is /// responsible for placing the `[N]` suffix after the field name. pub fn render_type(ty: &Type, ctx: RenderCtx) -> Result { diff --git a/src/backends/json.rs b/src/backends/json.rs index 8befc26a..86277501 100644 --- a/src/backends/json.rs +++ b/src/backends/json.rs @@ -260,24 +260,45 @@ impl JsonDocLink { } } -/// Context for resolving doc-comment links during conversion: the shared -/// resolver plus the scope of the module currently being converted. +/// Context for surfacing doc-comment links during conversion: the module's +/// resolved link table, produced once during semantic analysis. Links are +/// looked up by the doc-bearing node's location — conversion never re-scans +/// or re-resolves doc text, so it cannot diverge from what the compiler +/// validated. struct DocCx<'a> { - resolver: &'a crate::semantic::doc_links::DocLinkResolver, - scope: Vec, + links: &'a crate::semantic::doc_links::ModuleDocLinks, } impl DocCx<'_> { - /// Convert a doc comment into its markdown text and resolved links. - fn convert(&self, doc: &[String]) -> (Option, Vec) { + /// Convert a doc comment into its markdown text and the resolved links of + /// the doc block owned by the node at `location`. + fn convert( + &self, + doc: &[String], + location: &crate::span::ItemLocation, + ) -> (Option, Vec) { + Self::convert_resolved(doc, self.links.at(location)) + } + + /// Convert the module's own doc block (keyed separately from node docs — + /// see [`crate::semantic::doc_links::DocBlockKey`]). + fn convert_module_doc(&self, doc: &[String]) -> (Option, Vec) { + Self::convert_resolved(doc, self.links.module_doc()) + } + + fn convert_resolved( + doc: &[String], + resolved: &[crate::semantic::doc_links::ResolvedDocLink], + ) -> (Option, Vec) { let mut links: Vec = Vec::new(); - for text in crate::semantic::doc_links::extract_links(doc) { - if links.iter().any(|l| l.text == text) { + for link in resolved { + if links.iter().any(|l| l.text == link.text) { continue; } - if let Some(target) = self.resolver.resolve(&self.scope, &text) { - links.push(JsonDocLink::from_target(text, target)); - } + links.push(JsonDocLink::from_target( + link.text.clone(), + link.target.clone(), + )); } (doc_to_option(doc), links) } @@ -756,11 +777,10 @@ pub fn build( // predicate attached as structured data so downstream tooling can // render or filter per their own rules. let mut items = BTreeMap::new(); - for module in semantic_state.modules().values() { + for (module_path, module) in semantic_state.modules() { let bindings: BTreeMap<&str, ExternBindings> = module.extern_bindings().collect(); let cx = DocCx { - resolver: semantic_state.doc_link_resolver(), - scope: module.scope(), + links: semantic_state.module_doc_links(module_path), }; for definition in module.definitions(type_registry) { let binding = definition @@ -779,9 +799,9 @@ pub fn build( // module, so the loop above never emits them. Add them here so the viewer // can render the builtin types user code references. They carry no source // location (see `ItemDefinition::default`), and are public. + let predefined_links = Default::default(); let predefined_cx = DocCx { - resolver: semantic_state.doc_link_resolver(), - scope: Vec::new(), + links: &predefined_links, }; for (path, item) in type_registry.iter() { if item.category != ItemCategory::Predefined { @@ -901,7 +921,7 @@ fn convert_function_body(body: &FunctionBody) -> JsonFunctionBody { } fn convert_function(func: &Function, cx: &DocCx) -> JsonFunction { - let (doc, doc_links) = cx.convert(&func.doc); + let (doc, doc_links) = cx.convert(&func.doc, &func.location); JsonFunction { visibility: func.visibility.into(), name: func.name.clone(), @@ -947,7 +967,7 @@ fn convert_region( ) -> JsonRegion { let size = region.type_ref.size(type_registry).unwrap_or(0); let alignment = region.type_ref.alignment(type_registry).unwrap_or(1); - let (doc, doc_links) = cx.convert(®ion.doc); + let (doc, doc_links) = cx.convert(®ion.doc, ®ion.location); JsonRegion { visibility: region.visibility.into(), @@ -977,6 +997,7 @@ fn convert_type_definition( td: &TypeDefinition, type_registry: &TypeRegistry, cx: &DocCx, + item_location: &crate::span::ItemLocation, ) -> JsonTypeDefinition { // Calculate field offsets let mut current_offset = 0; @@ -990,7 +1011,7 @@ fn convert_type_definition( }) .collect(); - let (doc, doc_links) = cx.convert(&td.doc); + let (doc, doc_links) = cx.convert(&td.doc, item_location); JsonTypeDefinition { doc, doc_links, @@ -1012,7 +1033,7 @@ fn convert_type_definition( } fn convert_enum_variant(variant: &EnumVariant, cx: &DocCx) -> JsonEnumVariant { - let (doc, doc_links) = cx.convert(&variant.doc); + let (doc, doc_links) = cx.convert(&variant.doc, &variant.location); JsonEnumVariant { name: variant.name.clone(), value: variant.value, @@ -1027,8 +1048,9 @@ fn convert_enum_definition( type_registry: &TypeRegistry, parent_path: &ItemPath, cx: &DocCx, + item_location: &crate::span::ItemLocation, ) -> JsonEnumDefinition { - let (doc, doc_links) = cx.convert(&ed.doc); + let (doc, doc_links) = cx.convert(&ed.doc, item_location); let nested_items: Vec = type_registry .iter() .filter(|(p, _)| p.parent().as_ref() == Some(parent_path)) @@ -1058,7 +1080,7 @@ fn convert_enum_definition( } fn convert_bitflag_field(flag: &BitflagField, cx: &DocCx) -> JsonBitflag { - let (doc, doc_links) = cx.convert(&flag.doc); + let (doc, doc_links) = cx.convert(&flag.doc, &flag.location); JsonBitflag { name: flag.name.clone(), value: flag.value, @@ -1073,8 +1095,9 @@ fn convert_bitflags_definition( type_registry: &TypeRegistry, parent_path: &ItemPath, cx: &DocCx, + item_location: &crate::span::ItemLocation, ) -> JsonBitflagsDefinition { - let (doc, doc_links) = cx.convert(&bd.doc); + let (doc, doc_links) = cx.convert(&bd.doc, item_location); let nested_items: Vec = type_registry .iter() .filter(|(p, _)| p.parent().as_ref() == Some(parent_path)) @@ -1098,8 +1121,12 @@ fn convert_bitflags_definition( } } -fn convert_type_alias_definition(ta: &TypeAliasDefinition, cx: &DocCx) -> JsonTypeAliasDefinition { - let (doc, doc_links) = cx.convert(&ta.doc); +fn convert_type_alias_definition( + ta: &TypeAliasDefinition, + cx: &DocCx, + item_location: &crate::span::ItemLocation, +) -> JsonTypeAliasDefinition { + let (doc, doc_links) = cx.convert(&ta.doc, item_location); JsonTypeAliasDefinition { doc, doc_links, @@ -1107,8 +1134,12 @@ fn convert_type_alias_definition(ta: &TypeAliasDefinition, cx: &DocCx) -> JsonTy } } -fn convert_const_definition(cd: &SemanticConstDefinition, cx: &DocCx) -> JsonConstantDefinition { - let (doc, doc_links) = cx.convert(&cd.doc); +fn convert_const_definition( + cd: &SemanticConstDefinition, + cx: &DocCx, + item_location: &crate::span::ItemLocation, +) -> JsonConstantDefinition { + let (doc, doc_links) = cx.convert(&cd.doc, item_location); let value = convert_const_value(&cd.value, cx); JsonConstantDefinition { doc, @@ -1162,23 +1193,30 @@ fn convert_item( let kind = match &resolved.inner { - ItemDefinitionInner::Type(td) => { - JsonItemKind::Type(convert_type_definition(td, type_registry, cx)) - } - ItemDefinitionInner::Enum(ed) => { - JsonItemKind::Enum(convert_enum_definition(ed, type_registry, &item.path, cx)) - } + ItemDefinitionInner::Type(td) => JsonItemKind::Type(convert_type_definition( + td, + type_registry, + cx, + &item.location, + )), + ItemDefinitionInner::Enum(ed) => JsonItemKind::Enum(convert_enum_definition( + ed, + type_registry, + &item.path, + cx, + &item.location, + )), ItemDefinitionInner::Bitflags(bd) => JsonItemKind::Bitflags( - convert_bitflags_definition(bd, type_registry, &item.path, cx), + convert_bitflags_definition(bd, type_registry, &item.path, cx, &item.location), ), ItemDefinitionInner::TypeAlias(ta) => { - JsonItemKind::TypeAlias(convert_type_alias_definition(ta, cx)) + JsonItemKind::TypeAlias(convert_type_alias_definition(ta, cx, &item.location)) } ItemDefinitionInner::Constant(cd) => { - JsonItemKind::Constant(convert_const_definition(cd, cx)) + JsonItemKind::Constant(convert_const_definition(cd, cx, &item.location)) } ItemDefinitionInner::ExternValue(ev) => { - JsonItemKind::ExternValue(convert_extern_value_definition(ev, cx)) + JsonItemKind::ExternValue(convert_extern_value_definition(ev, cx, &item.location)) } }; @@ -1204,8 +1242,9 @@ fn convert_item( fn convert_extern_value_definition( ev: &SemanticExternValueDefinition, cx: &DocCx, + item_location: &crate::span::ItemLocation, ) -> JsonExternValueDefinition { - let (doc, doc_links) = cx.convert(&ev.doc); + let (doc, doc_links) = cx.convert(&ev.doc, item_location); JsonExternValueDefinition { doc, doc_links, @@ -1240,8 +1279,7 @@ fn build_module_hierarchy(semantic_state: &SemanticOutput) -> BTreeMap BTreeMap = module.splices.iter().map(convert_splice).collect(); - let (doc, doc_links) = cx.convert(module.doc()); + let (doc, doc_links) = cx.convert_module_doc(module.doc()); let json_module = JsonModule { doc, doc_links, diff --git a/src/backends/rust/mod.rs b/src/backends/rust/mod.rs index 4fca7430..940d9609 100644 --- a/src/backends/rust/mod.rs +++ b/src/backends/rust/mod.rs @@ -11,6 +11,7 @@ use crate::{ grammar::ItemPath, semantic::{ Module, SemanticOutput, TypeRegistry, + doc_links::{DocLinkTarget, ModuleDocLinks, ResolvedDocLink}, types::{ Argument, BitflagsDefinition, ConstDefinition as SemanticConstDefinition, ConstValue, EnumDefinition, ExternValueDefinition as SemanticExternValueDefinition, Function, @@ -111,10 +112,17 @@ pub fn write_module( // on the root file (lib.rs / the mounted-subtree root); emitting them on // the root also overrides a stricter host crate (the innermost level // wins), keeping the whole generated subtree quiet. + // + // `rustdoc::redundant_explicit_links`: every resolved doc link's + // destination is rewritten to the absolute path of its semantic target + // (see `DocLinkCx`), uniformly. For links rustdoc could have resolved from + // the label alone this is "redundant" — deciding when that holds would + // mean re-implementing rustdoc's own resolution, so the lint is allowed + // instead. if key.is_empty() { writeln!( raw_output, - "#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals, clippy::missing_safety_doc, clippy::unnecessary_cast, clippy::module_inception)]" + "#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals, clippy::missing_safety_doc, clippy::unnecessary_cast, clippy::module_inception, rustdoc::redundant_explicit_links)]" )?; } // Disable rustfmt on generated files to prevent the prettyplease-formatted code being reformatted @@ -125,50 +133,23 @@ pub fn write_module( // Collect all module paths for flattening nested item names. let module_paths: BTreeSet = semantic_state.modules().keys().cloned().collect(); - // Compute doc link imports and nested item rewrites before rendering - // module docs, so doc link references can be rewritten. - let module_scope = module.scope(); - let doc_links = semantic_state.doc_link_resolver().module_doc_links( - semantic_state.type_registry(), - semantic_state.modules(), - key, - ); - let doc_imports = &doc_links.imports; - let module_path_set: BTreeSet = semantic_state.modules().keys().cloned().collect(); - let mut cross_module_imports: Vec<&ItemPath> = Vec::new(); - let mut same_module_aliases: Vec<(&ItemPath, String)> = Vec::new(); - for p in doc_imports { - let declaring_len = find_module_prefix_len(p, &module_path_set); - let declaring_module: ItemPath = p.iter().take(declaring_len).cloned().collect(); - if &declaring_module == key { - if p.len() > key.len() + 1 { - let flat = flatten_type_name(p, &module_path_set); - let leaf = p.last().map(|s| s.as_str().to_string()).unwrap_or_default(); - if flat != leaf { - same_module_aliases.push((p, flat)); - } - } - } else { - cross_module_imports.push(p); - } - } - let mut nested_rewrites: std::collections::HashMap = { - let mut map = std::collections::HashMap::new(); - for (p, flat) in &same_module_aliases { - // The unflattened leaf is the item path's last segment — not - // `flat.rsplit('_')`, which would truncate an item name that itself - // contains an underscore. - let leaf = p.last().map(|s| s.as_str()).unwrap_or(flat.as_str()); - map.insert(leaf.to_string(), flat.clone()); - } - map + // Doc-link rewriting context: every resolved link's destination is + // rendered from its semantic target as an absolute crate path, so rustdoc + // resolves it without any doc-driven imports or aliases. + let prefix = options.rust_module_prefix.as_ref(); + let doc_cx = DocLinkCx { + links: semantic_state.module_doc_links(key), + type_registry: semantic_state.type_registry(), + module_paths: &module_paths, + module_path: key, + prefix, + root: match prefix { + Some(prefix) => format!("crate::{prefix}"), + None => "crate".to_string(), + }, }; - writeln!( - raw_output, - "{}", - doc_to_tokens(true, module.doc(), Some(&nested_rewrites)) - )?; + writeln!(raw_output, "{}", doc_cx.module_doc(module.doc()))?; // Emit the freestanding `__bitflags!` macro definition exactly once, on the // crate root, when the crate contains any bitflags. It is @@ -206,41 +187,6 @@ pub fn write_module( writeln!(raw_output, "pub mod {child};")?; } - // Rewrite cross-module doc-link destinations to absolute crate paths so - // rustdoc resolves them without any `use` imports. The path is flattened - // for nested items (e.g. `module::Outer::InnerEnum` → - // `crate::module::Outer_InnerEnum`), using the declaring module's prefix. - let mut cross_module_imports: Vec = - cross_module_imports.into_iter().cloned().collect(); - cross_module_imports.retain(|p| { - !module_scope.contains(p) && p.last().is_some_and(|s| is_plain_ident(s.as_str())) - }); - let prefix = options.rust_module_prefix.as_ref(); - let root = match prefix { - Some(prefix) => format!("crate::{prefix}"), - None => "crate".to_string(), - }; - // Map a canonical item path to its absolute Rust path, flattening nested - // item names (`module::Outer::Inner` → `crate::module::Outer_Inner`). - let to_rust_path = |p: &ItemPath| -> String { - let module_len = find_module_prefix_len(p, &module_path_set); - if p.len() > module_len + 1 { - let type_segments: Vec<&str> = p.iter().skip(module_len).map(|s| s.as_str()).collect(); - let module_part: Vec<&str> = p.iter().take(module_len).map(|s| s.as_str()).collect(); - let flat_name = type_segments.join("_"); - if module_part.is_empty() { - format!("{root}::{flat_name}") - } else { - format!("{root}::{}::{flat_name}", module_part.join("::")) - } - } else { - format!("{root}::{p}") - } - }; - for p in &cross_module_imports { - nested_rewrites.insert(p.to_string(), to_rust_path(p)); - } - // Emit explicit `pub use` re-exports. Each re-export is canonicalized (past // any re-export chain) to the defining item and rendered as an absolute // `pub use crate::…;`, so consumers of the generated crate can reach the @@ -251,27 +197,11 @@ pub fn write_module( if !type_registry.contains(&canonical) { continue; } - writeln!(raw_output, "pub use {};", to_rust_path(&canonical))?; - } - - // Extern values emit as `get_` accessors (a free fn when module-level, - // an inherent method when nested), so a doc link written against the value's - // logical name won't resolve. Rewrite each such link's destination to the - // accessor's Rust path, reusing the same accessor naming the emitter uses. - for (text, value_path) in &doc_links.extern_value_links { - let rust_path = extern_value_accessor_doc_path(value_path, key, &module_path_set, prefix); - nested_rewrites.insert(text.clone(), rust_path); - } - - // Generate `use FlatName as LeafName;` aliases for same-module nested - // items referenced in doc links, so rustdoc resolves [`LeafName`] to - // the flattened Rust identifier. - if !same_module_aliases.is_empty() { - for (p, flat) in &same_module_aliases { - let leaf = p.last().map(|s| s.as_str()).unwrap_or(flat.as_str()); - writeln!(raw_output, "#[allow(unused_imports)]")?; - writeln!(raw_output, "use {flat} as {leaf};")?; - } + writeln!( + raw_output, + "pub use {};", + doc_cx.absolute_item_path(&canonical) + )?; } let cfg_pass = |cfg: &Option| match cfg { @@ -313,7 +243,7 @@ pub fn write_module( options, &extern_rust_names, &module_paths, - &nested_rewrites, + &doc_cx, )? )?; } @@ -324,7 +254,7 @@ pub fn write_module( .iter() .filter(|f| !f.is_internal()) .filter(|f| cfg_pass(&f.cfg)) - .map(|f| build_function(f, options, false, &module_paths)) + .map(|f| build_function(f, options, false, &module_paths, &doc_cx)) .collect::>>()?; for func in freestanding_functions { writeln!(raw_output, "{func}")?; @@ -382,7 +312,7 @@ fn build_item( options: &crate::BuildOptions, extern_rust_names: &HashMap, module_paths: &BTreeSet, - nested_rewrites: &std::collections::HashMap, + doc_cx: &DocLinkCx, ) -> Result { let resolved = definition .resolved() @@ -417,7 +347,7 @@ fn build_item( cfg_ctx, options, module_paths, - nested_rewrites, + doc_cx, ), IDI::Enum(ed) => build_enum( type_registry, @@ -429,7 +359,7 @@ fn build_item( cfg_ctx, options, module_paths, - nested_rewrites, + doc_cx, ), IDI::Bitflags(bd) => build_bitflags( type_registry, @@ -440,7 +370,7 @@ fn build_item( location, options.rust_module_prefix.as_ref(), module_paths, - nested_rewrites, + doc_cx, ), IDI::TypeAlias(ta) => build_type_alias( type_registry, @@ -451,16 +381,17 @@ fn build_item( type_parameters, options.rust_module_prefix.as_ref(), module_paths, - nested_rewrites, + doc_cx, ), - IDI::Constant(cd) => build_const(path, visibility, cd, location, module_paths), + IDI::Constant(cd) => build_const(path, visibility, cd, location, module_paths, doc_cx), IDI::ExternValue(ev) => build_extern_value( path, visibility, ev, + location, options.rust_module_prefix.as_ref(), module_paths, - nested_rewrites, + doc_cx, ), }, ItemCategory::Predefined => Ok(quote! {}), @@ -494,12 +425,12 @@ fn build_type( alignment: usize, visibility: Visibility, type_definition: &TypeDefinition, - _location: &ItemLocation, + location: &ItemLocation, type_parameters: &[String], cfg_ctx: &crate::parser::cfg::CfgContext, options: &crate::BuildOptions, module_paths: &BTreeSet, - nested_rewrites: &std::collections::HashMap, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let name = &name; @@ -520,7 +451,7 @@ fn build_type( } = type_definition; let visibility = visibility_to_tokens(visibility); - let doc = doc_to_tokens(false, doc, Some(nested_rewrites)); + let doc = doc_cx.node(doc, location); let mut fields = regions .iter() .map(|r| { @@ -543,7 +474,7 @@ fn build_type( let field_ident = str_to_ident(field_name); let visibility = visibility_to_tokens(*visibility); let syn_type = sa_type_to_syn_type(type_ref, prefix, Some(module_paths))?; - let doc = doc_to_tokens(false, doc, Some(nested_rewrites)); + let doc = doc_cx.node(doc, location); Ok(quote! { #doc #visibility #field_ident: #syn_type @@ -604,7 +535,7 @@ fn build_type( .iter() .filter(|f| !f.is_internal()) .filter(|f| cfg_pass(&f.cfg)) - .map(|f| build_function(f, options, true, module_paths)) + .map(|f| build_function(f, options, true, module_paths, doc_cx)) .collect::>>()?; let vftable_function_impl = vftable @@ -614,7 +545,7 @@ fn build_type( .iter() .filter(|f| !f.is_internal()) .filter(|f| cfg_pass(&f.cfg)) - .map(|f| build_function(f, options, true, module_paths)) + .map(|f| build_function(f, options, true, module_paths, doc_cx)) .collect::>>() }) .transpose()? @@ -757,10 +688,10 @@ fn build_type( }; // Emit nested constants as associated constants in an impl block - let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths); + let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths, doc_cx); // Emit nested extern values as associated `get_*` accessors in an impl block let nested_extern_value_impls = - build_nested_extern_value_impls(type_registry, path, module_paths, nested_rewrites); + build_nested_extern_value_impls(type_registry, path, module_paths, doc_cx); Ok(quote! { #derives @@ -789,11 +720,11 @@ fn build_enum( size: usize, visibility: Visibility, enum_definition: &EnumDefinition, - _location: &ItemLocation, + location: &ItemLocation, cfg_ctx: &crate::parser::cfg::CfgContext, options: &crate::BuildOptions, module_paths: &BTreeSet, - nested_rewrites: &std::collections::HashMap, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let name = &name; @@ -815,7 +746,7 @@ fn build_enum( let name_ident = str_to_ident(name.as_str()); let visibility = visibility_to_tokens(visibility); - let doc = doc_to_tokens(false, doc, Some(nested_rewrites)); + let doc = doc_cx.node(doc, location); let size_check_impl = generate_size_check(name.as_str(), size); @@ -866,7 +797,7 @@ fn build_enum( .iter() .filter(|f| !f.is_internal()) .filter(|f| cfg_pass(&f.cfg)) - .map(|f| build_function(f, options, true, module_paths)) + .map(|f| build_function(f, options, true, module_paths, doc_cx)) .collect::>>()?; let associated_impl = if !associated_functions_impl.is_empty() { @@ -880,10 +811,10 @@ fn build_enum( }; // Emit nested constants as associated constants in an impl block - let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths); + let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths, doc_cx); // Emit nested extern values as associated `get_*` accessors in an impl block let nested_extern_value_impls = - build_nested_extern_value_impls(type_registry, path, module_paths, nested_rewrites); + build_nested_extern_value_impls(type_registry, path, module_paths, doc_cx); Ok(quote! { #[repr(#syn_type)] @@ -907,10 +838,10 @@ fn build_bitflags( size: usize, visibility: Visibility, bitflags_definition: &BitflagsDefinition, - _location: &ItemLocation, + location: &ItemLocation, prefix: Option<&ItemPath>, module_paths: &BTreeSet, - nested_rewrites: &std::collections::HashMap, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let name = &name; @@ -928,7 +859,7 @@ fn build_bitflags( let name_ident = str_to_ident(name.as_str()); let visibility = visibility_to_tokens(visibility); - let doc = doc_to_tokens(false, doc, Some(nested_rewrites)); + let doc = doc_cx.node(doc, location); let size_check_impl = generate_size_check(name.as_str(), size); @@ -968,10 +899,10 @@ fn build_bitflags( }); // Emit nested constants as associated constants in an impl block - let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths); + let nested_const_impls = build_nested_const_impls(type_registry, path, module_paths, doc_cx); // Emit nested extern values as associated `get_*` accessors in an impl block let nested_extern_value_impls = - build_nested_extern_value_impls(type_registry, path, module_paths, nested_rewrites); + build_nested_extern_value_impls(type_registry, path, module_paths, doc_cx); Ok(quote! { crate::__bitflags! { @@ -994,11 +925,11 @@ fn build_type_alias( path: &ItemPath, visibility: Visibility, type_alias_definition: &TypeAliasDefinition, - _location: &ItemLocation, + location: &ItemLocation, type_parameters: &[String], prefix: Option<&ItemPath>, module_paths: &BTreeSet, - nested_rewrites: &std::collections::HashMap, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let name = &name; @@ -1007,7 +938,7 @@ fn build_type_alias( let name_ident = str_to_ident(name.as_str()); let visibility = visibility_to_tokens(visibility); - let doc = doc_to_tokens(false, doc, Some(nested_rewrites)); + let doc = doc_cx.node(doc, location); let target_type = sa_type_to_syn_type(target, prefix, Some(module_paths))?; let generic_params = build_generic_params(type_parameters); @@ -1135,14 +1066,15 @@ fn build_const( path: &ItemPath, visibility: Visibility, const_definition: &SemanticConstDefinition, - _location: &ItemLocation, + location: &ItemLocation, module_paths: &BTreeSet, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let name_ident = str_to_ident(name.as_str()); let visibility = visibility_to_tokens(visibility); let type_ = sa_type_to_syn_type(&const_definition.type_, None, Some(module_paths))?; - let doc = doc_to_tokens(false, &const_definition.doc, None); + let doc = doc_cx.node(&const_definition.doc, location); let value_tokens = const_value_to_tokens( &const_definition.value, &const_definition.type_, @@ -1161,6 +1093,7 @@ fn build_nested_const_impls( type_registry: &TypeRegistry, parent_path: &ItemPath, module_paths: &BTreeSet, + doc_cx: &DocLinkCx, ) -> Option { use ItemDefinitionInner as IDI; @@ -1184,7 +1117,7 @@ fn build_nested_const_impls( Ok(t) => t, Err(_) => continue, }; - let doc = doc_to_tokens(false, &cd.doc, None); + let doc = doc_cx.node(&cd.doc, &item.location); let value_tokens = const_value_to_tokens(&cd.value, &cd.type_, module_paths); const_items.push(quote! { @@ -1210,6 +1143,7 @@ fn build_function( options: &crate::BuildOptions, in_impl: bool, module_paths: &BTreeSet, + doc_cx: &DocLinkCx, ) -> Result { let prefix = options.rust_module_prefix.as_ref(); // External-body methods declare their existence in pyxis but get their @@ -1221,7 +1155,7 @@ fn build_function( return Ok(proc_macro2::TokenStream::new()); } let name = str_to_ident(&function.name); - let doc = doc_to_tokens(false, &function.doc, None); + let doc = doc_cx.node(&function.doc, &function.location); let arguments = function .arguments @@ -1364,16 +1298,17 @@ fn build_extern_value( path: &ItemPath, visibility: Visibility, ev: &SemanticExternValueDefinition, + location: &ItemLocation, prefix: Option<&ItemPath>, module_paths: &BTreeSet, - nested_rewrites: &HashMap, + doc_cx: &DocLinkCx, ) -> Result { let name = flatten_type_name(path, module_paths); let visibility = visibility_to_tokens(visibility); let function_ident = str_to_ident(&extern_value_accessor_name(&name)); let type_ = sa_type_to_syn_type(&ev.type_, prefix, Some(module_paths))?; let address = hex_literal(ev.address); - let doc = doc_to_tokens(false, &ev.doc, Some(nested_rewrites)); + let doc = doc_cx.node(&ev.doc, location); Ok(quote! { #doc @@ -1391,7 +1326,7 @@ fn build_nested_extern_value_impls( type_registry: &TypeRegistry, parent_path: &ItemPath, module_paths: &BTreeSet, - nested_rewrites: &HashMap, + doc_cx: &DocLinkCx, ) -> Option { use ItemDefinitionInner as IDI; @@ -1415,7 +1350,7 @@ fn build_nested_extern_value_impls( Err(_) => continue, }; let address = hex_literal(ev.address); - let doc = doc_to_tokens(false, &ev.doc, Some(nested_rewrites)); + let doc = doc_cx.node(&ev.doc, &item.location); items.push(quote! { #doc @@ -1441,16 +1376,6 @@ fn str_to_ident(s: &str) -> syn::Ident { quote::format_ident!("{}", s) } -/// Whether `s` is a plain Rust identifier (no generics, operators, etc.), so it -/// can appear verbatim in a `use` path. -fn is_plain_ident(s: &str) -> bool { - let mut chars = s.chars(); - chars - .next() - .is_some_and(|c| c.is_ascii_alphabetic() || c == '_') - && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') -} - /// Flatten a nested item path to a Rust-safe identifier by joining type-nesting /// segments with `_`. Module segments are identified by matching against known /// module paths; everything after the module prefix is a type segment. @@ -1745,13 +1670,16 @@ fn visibility_to_tokens(visibility: Visibility) -> proc_macro2::TokenStream { fn doc_to_tokens( is_module_doc: bool, doc: &[String], - nested_rewrites: Option<&std::collections::HashMap>, + links: Option<(&DocLinkCx, &[ResolvedDocLink])>, ) -> proc_macro2::TokenStream { if doc.is_empty() { return proc_macro2::TokenStream::new(); }; let doc_attrs = doc.iter().map(|line| { - let rewritten = rewrite_doc_links(line, nested_rewrites); + let rewritten = match links { + Some((cx, block)) => cx.rewrite_line(line, block), + None => line.clone(), + }; if is_module_doc { quote! { #![doc = #rewritten] } } else { @@ -1763,74 +1691,133 @@ fn doc_to_tokens( } } -/// Rewrite a single `::`-separated intra-doc link path to use flattened Rust -/// names for nested items. When a segment matches a rewrite key, it's replaced -/// with the flattened name and all preceding segments are dropped (they're the -/// parent-type prefix already baked into the flattened name), while trailing -/// member-access segments are preserved. For example, with `Inner → Outer_Inner`: -/// `Inner` → `Outer_Inner`, `Inner::A` → `Outer_Inner::A`, and -/// `Outer::Inner::CONST` → `Outer_Inner::CONST`. +/// Context for rewriting intra-doc links in emitted docs. /// -/// Cross-module paths are rewritten to absolute crate paths via an exact -/// full-path match before the per-segment loop. -fn rewrite_doc_link_path( - content: &str, - rewrites: &std::collections::HashMap, -) -> String { - // First, try an exact full-path match (for cross-module imports). - if let Some(target) = rewrites.get(content) { - return target.clone(); +/// Each link's *resolved target* — determined once during semantic analysis +/// and stored in the module's [`ModuleDocLinks`] table — is rendered as an +/// absolute Rust path (`crate::module::Outer_Inner::member`), flattening +/// nested-item names and substituting extern-value accessors. Rewriting from +/// the target rather than the written text means the destination is always +/// what the link actually resolved to: no leaf-name rewrite maps that can +/// collide, no doc-driven `use` imports for rustdoc's benefit. +struct DocLinkCx<'a> { + links: &'a ModuleDocLinks, + type_registry: &'a TypeRegistry, + module_paths: &'a BTreeSet, + /// The module being emitted; extern-value accessor paths in the same + /// module stay relative. + module_path: &'a ItemPath, + prefix: Option<&'a ItemPath>, + /// `crate` or `crate::`. + root: String, +} + +impl DocLinkCx<'_> { + /// Doc tokens for the doc block owned by the node at `location`. + fn node(&self, doc: &[String], location: &ItemLocation) -> proc_macro2::TokenStream { + doc_to_tokens(false, doc, Some((self, self.links.at(location)))) } - let segments: Vec<&str> = content.split("::").collect(); - if segments.len() == 1 { - return rewrites - .get(segments[0]) - .cloned() - .unwrap_or_else(|| content.to_string()); + + /// Doc tokens for the module's own (`//!`) doc block. + fn module_doc(&self, doc: &[String]) -> proc_macro2::TokenStream { + doc_to_tokens(true, doc, Some((self, self.links.module_doc()))) } - for (i, seg) in segments.iter().enumerate() { - if let Some(flat) = rewrites.get(*seg) { - let rest = &segments[i + 1..]; - return if rest.is_empty() { - flat.clone() + + /// The absolute Rust path of an item: `{root}::{module}::{FlatName}`, + /// flattening nested-item segments (`module::Outer::Inner` → + /// `crate::module::Outer_Inner`). + fn absolute_item_path(&self, path: &ItemPath) -> String { + let module_len = find_module_prefix_len(path, self.module_paths); + let root = &self.root; + if path.len() > module_len + 1 { + let flat_name = flatten_type_name(path, self.module_paths); + let module_part: Vec<&str> = path.iter().take(module_len).map(|s| s.as_str()).collect(); + if module_part.is_empty() { + format!("{root}::{flat_name}") } else { - format!("{}::{}", flat, rest.join("::")) - }; + format!("{root}::{}::{flat_name}", module_part.join("::")) + } + } else { + format!("{root}::{path}") } } - content.to_string() -} -/// Rewrite intra-doc link references in a doc comment line to use flattened -/// Rust names for nested items. Handles both the shortcut form (`` [`InnerEnum`] `` -/// → `` [`Outer_InnerEnum`] ``) and the inline form's destination -/// (`[label](Outer::InnerEnum)` → `[label](Outer_InnerEnum)`) — the latter -/// matters because rustdoc resolves the destination, not the label, and nested -/// types are emitted under flattened names. -/// -/// Link detection is shared with the compiler and LSP via -/// [`scan_links`](crate::semantic::doc_links::scan_links); each link's precise -/// `path_region` is substituted in place (right-to-left so earlier offsets stay -/// valid). Bare `[Path]` shortcuts are left alone — the compiler doesn't import -/// them, so rustdoc couldn't resolve a rewritten one anyway. -fn rewrite_doc_links( - line: &str, - nested_rewrites: Option<&std::collections::HashMap>, -) -> String { - use crate::semantic::doc_links::DocLinkSyntax; - let Some(rewrites) = nested_rewrites else { - return line.to_string(); - }; - let mut result = line.to_string(); - let mut links = crate::semantic::doc_links::scan_links(line); - links.retain(|l| l.syntax != DocLinkSyntax::PlainShortcut); - for link in links.into_iter().rev() { - let rewritten = rewrite_doc_link_path(&link.path, rewrites); - if rewritten != link.path { - result.replace_range(link.path_region.0..link.path_region.1, &rewritten); + /// Render a resolved target as the destination rustdoc should see, or + /// `None` to leave the written link untouched (predefined types, which + /// rustdoc resolves natively as primitives). + fn render_target(&self, target: &DocLinkTarget) -> Option { + use crate::semantic::doc_links::DocLinkMemberKind; + match target { + DocLinkTarget::Item(path) => { + let predefined = self + .type_registry + .get(path, &ItemLocation::internal()) + .is_ok_and(|i| i.category == crate::semantic::types::ItemCategory::Predefined); + if predefined { + return None; + } + Some(self.absolute_item_path(path)) + } + DocLinkTarget::Member { item, name, kind } => match kind { + DocLinkMemberKind::ExternValue => Some(self.accessor_path(item, name)), + _ => Some(format!("{}::{name}", self.absolute_item_path(item))), + }, + DocLinkTarget::Function { module, name } => Some(if module.is_empty() { + format!("{}::{name}", self.root) + } else { + format!("{}::{module}::{name}", self.root) + }), + DocLinkTarget::ExternValue { module, name } => Some(self.accessor_path(module, name)), + } + } + + /// The rustdoc path of an extern value's `get_` accessor, given the + /// value's parent (module or type) and name. + fn accessor_path(&self, parent: &ItemPath, name: &str) -> String { + let value_path = parent.join(crate::grammar::ItemPathSegment::from(name)); + extern_value_accessor_doc_path( + &value_path, + self.module_path, + self.module_paths, + self.prefix, + ) + } + + /// Rewrite every resolved link in `line` to its rendered destination. + /// + /// Link spans come from [`scan_links`](crate::semantic::doc_links::scan_links) + /// (shared with the compiler and LSP) and are substituted right-to-left so + /// earlier offsets stay valid. An inline link keeps its label and gets its + /// destination replaced; a code shortcut becomes an inline link so its + /// visible label survives the rewrite. Bare `[Path]` shortcuts aren't + /// resolved by the compiler and are left alone. + fn rewrite_line(&self, line: &str, block: &[ResolvedDocLink]) -> String { + use crate::semantic::doc_links::DocLinkSyntax; + if block.is_empty() { + return line.to_string(); + } + let mut result = line.to_string(); + let mut scanned = crate::semantic::doc_links::scan_links(line); + scanned.retain(|l| l.syntax != DocLinkSyntax::PlainShortcut); + for link in scanned.into_iter().rev() { + let Some(resolved) = block.iter().find(|r| r.text == link.path) else { + continue; + }; + let Some(dest) = self.render_target(&resolved.target) else { + continue; + }; + match link.syntax { + DocLinkSyntax::Inline => { + result.replace_range(link.path_region.0..link.path_region.1, &dest); + } + DocLinkSyntax::CodeShortcut | DocLinkSyntax::PlainShortcut => { + let label = &line[link.label_region.0..link.label_region.1]; + result.replace_range(link.link.0..link.link.1, &format!("[{label}]({dest})")); + } + } } + result } - result } fn hex_literal(value: impl Into) -> proc_macro2::Literal { diff --git a/src/semantic/doc_links.rs b/src/semantic/doc_links.rs index 0ea24eb6..8acbd25a 100644 --- a/src/semantic/doc_links.rs +++ b/src/semantic/doc_links.rs @@ -1,10 +1,12 @@ //! Resolution of rustdoc-style intra-doc links embedded in doc comments, e.g. -//! `[`Type`]`, `[`Type::method`]`, `[`Enum::VARIANT`]`, and the inline form +//! `[`Type`]`, `[`Type::method`]`, `[`Self::member`]`, and the inline form //! `` [label](Type::method) ``. //! -//! Resolution happens at the semantic layer (after every item and function is -//! resolved) so links are validated up-front; the resolver is then reused by -//! the backends to surface the resolved targets. +//! Every link is resolved exactly once, during semantic analysis +//! ([`resolve_all`]) — validation is that pass's failure path, and the +//! resulting per-module [`ModuleDocLinks`] tables (keyed by doc block) are +//! what the backends consume to rewrite or surface links. The [`DocLinkResolver`] +//! itself is also used live by the LSP to resolve links at edit time. use std::collections::{BTreeMap, BTreeSet}; @@ -36,43 +38,6 @@ pub enum DocLinkTarget { ExternValue { module: ItemPath, name: String }, } -impl DocLinkTarget { - /// The absolute path of the base item/function/extern to import so a Rust - /// consumer (rustdoc) can resolve the link. - pub fn import_path(&self) -> ItemPath { - match self { - DocLinkTarget::Item(path) => path.clone(), - DocLinkTarget::Member { item, .. } => item.clone(), - DocLinkTarget::Function { module, name } - | DocLinkTarget::ExternValue { module, name } => { - module.join(ItemPathSegment::from(name.clone())) - } - } - } - - /// If this link points at an extern value, its full item path — `module::name` - /// for a module-level one, `Parent::name` for a nested one. `None` for any - /// other target. - /// - /// Extern values emit as `get_` accessors rather than an item named - /// ``, so a backend can't just resolve the logical path; it needs the - /// value's path to compute the accessor's path (see the Rust backend's doc - /// link rewriting). - pub fn extern_value_path(&self) -> Option { - match self { - DocLinkTarget::ExternValue { module, name } => { - Some(module.join(ItemPathSegment::from(name.clone()))) - } - DocLinkTarget::Member { - item, - name, - kind: DocLinkMemberKind::ExternValue, - } => Some(item.join(ItemPathSegment::from(name.clone()))), - _ => None, - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DocLinkMemberKind { Method, @@ -84,19 +49,95 @@ pub enum DocLinkMemberKind { ExternValue, } -/// The intra-doc links referenced across a module's documentation, gathered by -/// [`DocLinkResolver::module_doc_links`] for backend rewriting. -#[derive(Debug, Clone, Default)] +/// A parsed intra-doc link path: an optional leading `Self` plus the remaining +/// `::`-separated segments. +/// +/// This is the *only* place link text is split into segments — everything +/// downstream of [`DocLinkPath::parse`] works with the structured form, so the +/// resolution path never re-derives structure from strings. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DocLinkPath { + /// Whether the written path began with `Self` (bare `Self` or `Self::…`). + pub self_prefixed: bool, + /// The path segments after any `Self` prefix. + pub segments: Vec, +} + +impl DocLinkPath { + pub fn parse(text: &str) -> Self { + let mut parts = text.split("::").peekable(); + let self_prefixed = parts.peek() == Some(&"Self"); + if self_prefixed { + parts.next(); + } + DocLinkPath { + self_prefixed, + segments: parts.map(ItemPathSegment::from).collect(), + } + } +} + +/// A single doc link resolved to its target, alongside the exact path text +/// written in the source — the text a backend substitutes when rewriting the +/// link destination. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ResolvedDocLink { + pub text: String, + pub target: DocLinkTarget, +} + +/// Identity of one doc block within a module, for keying resolved links. +/// +/// The module's own (`//!`-style) doc block gets a dedicated variant rather +/// than being keyed by `Module::location()`: that location is a *proxy* +/// borrowed from the module's first item (see `Module::from_ast`), so using +/// it as a key would collide with that item's own doc block. Every other +/// doc-bearing node is keyed by its source location, which is unique — no two +/// distinct nodes share an identical full span. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DocBlockKey { + /// The module's own doc block. + Module, + /// A doc-bearing node (item, field, function, variant, flag, …) at this + /// source location. + Node(ItemLocation), +} + +/// Every resolved intra-doc link in one module's documentation, keyed by the +/// owning doc block. Produced once by [`resolve_all`] during semantic +/// analysis; backends look their links up here rather than re-scanning and +/// re-resolving doc text with locally-reconstructed context. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] pub struct ModuleDocLinks { - /// Absolute paths of every item/function/extern referenced by a link, to be - /// imported so rustdoc resolves them. - pub imports: BTreeSet, - /// `(written link text, extern-value item path)` for each link pointing at - /// an extern value. The backend rewrites the link destination to the emitted - /// `get_` accessor rather than the value's logical name. - pub extern_value_links: Vec<(String, ItemPath)>, + pub by_block: BTreeMap>, +} + +impl ModuleDocLinks { + /// The resolved links of the doc block owned by the node at `location`. + pub fn at(&self, location: &ItemLocation) -> &[ResolvedDocLink] { + self.by_block + .get(&DocBlockKey::Node(*location)) + .map(Vec::as_slice) + .unwrap_or_default() + } + + /// The resolved links of the module's own doc block. + pub fn module_doc(&self) -> &[ResolvedDocLink] { + self.by_block + .get(&DocBlockKey::Module) + .map(Vec::as_slice) + .unwrap_or_default() + } + + /// Iterate every resolved link in the module. + pub fn iter(&self) -> impl Iterator { + self.by_block.values().flatten() + } } +/// The resolved doc links of every module in the crate, keyed by module path. +pub type DocLinks = BTreeMap; + #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum ItemMembers { Type { @@ -285,59 +326,128 @@ impl DocLinkResolver { /// Resolve a written link path (e.g. `Action`, `Type::method`) against a /// module scope. Returns `None` if it doesn't resolve to anything. - pub fn resolve(&self, scope: &[ItemPath], path_str: &str) -> Option { + /// + /// `enclosing_type` is the path of the item whose emitted docs will contain + /// the link — the type/enum/bitflags itself for its own and its members' + /// docs, or the *parent* type for a nested constant/extern value (their + /// docs land in the parent's `impl` block). It substitutes a `Self` prefix; + /// `None` at module scope, where `Self` doesn't resolve. + pub fn resolve( + &self, + scope: &[ItemPath], + path: &DocLinkPath, + enclosing_type: Option<&ItemPath>, + ) -> Option { + // Substitute a `Self` prefix with the enclosing type's segments. + let owned_segments; + let path: &[ItemPathSegment] = if path.self_prefixed { + let enclosing = enclosing_type?; + owned_segments = enclosing + .iter() + .chain(path.segments.iter()) + .cloned() + .collect::>(); + &owned_segments + } else { + &path.segments + }; + // 1. The whole path as a type. A nested constant is skipped here so it // falls through to the `Type::member` branch and resolves as a // member of its parent — the Rust backend emits it as an associated // const, so it has no importable free-item path of its own. - if let Some(item_path) = self.find_item(scope, path_str) + if let Some(item_path) = self.find_item(scope, path) && !self.nested_constant_paths.contains(&item_path) && !self.extern_value_paths.contains(&item_path) { return Some(DocLinkTarget::Item(item_path)); } // 2. `Type::member`. - if let Some((prefix, member)) = path_str.rsplit_once("::") - && let Some(item_path) = self.find_item(scope, prefix) - && let Some(kind) = self.find_member(&item_path, member) - { - return Some(DocLinkTarget::Member { - item: item_path, - name: member.to_string(), - kind, - }); + if path.len() >= 2 { + let (prefix, member) = path.split_at(path.len() - 1); + let member = member[0].as_str(); + if let Some(item_path) = self.find_item(scope, prefix) + && let Some(kind) = self.find_member(&item_path, member) + { + return Some(DocLinkTarget::Member { + item: item_path, + name: member.to_string(), + kind, + }); + } } // 3/4. A module-level freestanding function or extern value — the // current module first, then any module in the crate (the backend // imports it, like types). - if !path_str.contains("::") { - if let Some(module) = self.find_in_modules(scope, &self.module_functions, path_str) { + if path.len() == 1 { + let name = path[0].as_str(); + if let Some(module) = self.find_in_modules(scope, &self.module_functions, name) { return Some(DocLinkTarget::Function { module, - name: path_str.to_string(), + name: name.to_string(), }); } - if let Some(module) = self.find_in_modules(scope, &self.module_extern_values, path_str) - { + if let Some(module) = self.find_in_modules(scope, &self.module_extern_values, name) { return Some(DocLinkTarget::ExternValue { module, - name: path_str.to_string(), + name: name.to_string(), }); } + } else if path.len() >= 2 { + // Qualified: resolve the module path, then check that module. + let name = path[path.len() - 1].as_str(); + let module_segments = &path[..path.len() - 1]; + let bases = std::iter::once(ItemPath::empty()).chain(scope.iter().cloned()); + for base in bases { + let mut full = base; + for seg in module_segments { + full.push(seg.clone()); + } + if let Some(fns) = self.module_functions.get(&full) + && fns.iter().any(|n| n == name) + { + return Some(DocLinkTarget::Function { + module: full, + name: name.to_string(), + }); + } + if let Some(evs) = self.module_extern_values.get(&full) + && evs.iter().any(|n| n == name) + { + return Some(DocLinkTarget::ExternValue { + module: full, + name: name.to_string(), + }); + } + } } None } + /// The current module within a resolution scope: the first entry that + /// actually is a module. + /// + /// A scope is not just module paths — resolving a doc block inside a type + /// prepends the type's own path (so bare references to its nested items + /// resolve), and the tail carries `use`-imported item paths. Assuming + /// "first entry = current module" mis-anchors same-module preference at + /// the type path for those blocks, silently changing which of several + /// same-named crate-wide candidates wins. + fn current_module<'s>(&self, scope: &'s [ItemPath]) -> Option<&'s ItemPath> { + scope + .iter() + .find(|ip| self.module_functions.contains_key(ip)) + } + /// Find the module that declares a member with `name` in `by_module`, - /// preferring the current module (scope's first entry). + /// preferring the current module. fn find_in_modules( &self, scope: &[ItemPath], by_module: &BTreeMap>, name: &str, ) -> Option { - scope - .first() + self.current_module(scope) .filter(|m| { by_module .get(m) @@ -356,13 +466,14 @@ impl DocLinkResolver { /// crate-wide (any accessible type with that name) since the Rust backend /// imports doc-referenced types regardless of the current `use`s; a /// `::`-qualified name is resolved root- or scope-relative. - fn find_item(&self, scope: &[ItemPath], path_str: &str) -> Option { - let from_module = scope.first(); + fn find_item(&self, scope: &[ItemPath], segments: &[ItemPathSegment]) -> Option { + let from_module = self.current_module(scope); - if !path_str.contains("::") { + if segments.len() == 1 { // 1. A type directly imported into scope wins. + let name = segments[0].as_str(); if let Some(p) = scope.iter().rev().find(|ip| { - self.items.contains_key(ip) && ip.last().map(|s| s.as_str()) == Some(path_str) + self.items.contains_key(ip) && ip.last().map(|s| s.as_str()) == Some(name) }) { return Some(p.clone()); } @@ -372,8 +483,7 @@ impl DocLinkResolver { .items .keys() .filter(|ip| { - ip.last().map(|s| s.as_str()) == Some(path_str) - && self.can_access(from_module, ip) + ip.last().map(|s| s.as_str()) == Some(name) && self.can_access(from_module, ip) }) .collect(); candidates.sort_by_key(|ip| { @@ -384,12 +494,10 @@ impl DocLinkResolver { } // Qualified path: try it root-relative or relative to a scope module. - let segments: Vec = - path_str.split("::").map(ItemPathSegment::from).collect(); let bases = std::iter::once(ItemPath::empty()).chain(scope.iter().cloned()); for base in bases { let mut full = base.clone(); - for seg in &segments { + for seg in segments { full.push(seg.clone()); } if self.items.contains_key(&full) && self.can_access(from_module, &full) { @@ -415,133 +523,6 @@ impl DocLinkResolver { } } - /// Collect every intra-doc link referenced anywhere in `module_path`'s - /// documentation (its own doc, its items + their members, its functions) — - /// the item paths to import so rustdoc resolves them, plus the extern-value - /// links the Rust backend rewrites to `get_` accessors. See - /// [`ModuleDocLinks`]. - pub fn module_doc_links( - &self, - type_registry: &TypeRegistry, - modules: &BTreeMap, - module_path: &ItemPath, - ) -> ModuleDocLinks { - let Some(module) = modules.get(module_path) else { - return ModuleDocLinks::default(); - }; - let scope = module.scope(); - let mut links = ModuleDocLinks::default(); - - self.add_doc_imports(&scope, module.doc(), &mut links); - for f in module.functions() { - self.add_doc_imports(&scope, &f.doc, &mut links); - } - // Extern values' docs (including module-level ones) are collected by the - // registry walk below. - - for (path, item) in type_registry.iter() { - if path.parent().as_ref() != Some(module_path) { - continue; - } - let Some(resolved) = item.resolved() else { - continue; - }; - match &resolved.inner { - ItemDefinitionInner::Type(td) => { - // Augment scope with the type's own path so bare - // references to nested items (e.g. [InnerEnum]) resolve. - let type_scope: Vec = std::iter::once(path.clone()) - .chain(scope.iter().cloned()) - .collect(); - self.add_doc_imports(&type_scope, &td.doc, &mut links); - for r in &td.regions { - self.add_doc_imports(&type_scope, &r.doc, &mut links); - } - for f in &td.associated_functions { - self.add_doc_imports(&type_scope, &f.doc, &mut links); - } - if let Some(v) = &td.vftable { - for f in &v.functions { - self.add_doc_imports(&type_scope, &f.doc, &mut links); - } - } - // Also scan doc comments on nested items - for nested_path in &td.nested_item_paths { - if let Some(nested_item) = type_registry - .get(nested_path, &ItemLocation::internal()) - .ok() - && let Some(nested_resolved) = nested_item.resolved() - { - match &nested_resolved.inner { - ItemDefinitionInner::Type(ntd) => { - self.add_doc_imports(&type_scope, &ntd.doc, &mut links); - } - ItemDefinitionInner::Enum(ned) => { - self.add_doc_imports(&type_scope, &ned.doc, &mut links); - for v in &ned.variants { - self.add_doc_imports(&type_scope, &v.doc, &mut links); - } - } - ItemDefinitionInner::Bitflags(nbd) => { - self.add_doc_imports(&type_scope, &nbd.doc, &mut links); - for f in &nbd.flags { - self.add_doc_imports(&type_scope, &f.doc, &mut links); - } - } - ItemDefinitionInner::TypeAlias(nta) => { - self.add_doc_imports(&type_scope, &nta.doc, &mut links); - } - ItemDefinitionInner::Constant(ncd) => { - self.add_doc_imports(&type_scope, &ncd.doc, &mut links); - } - ItemDefinitionInner::ExternValue(nev) => { - self.add_doc_imports(&type_scope, &nev.doc, &mut links); - } - } - } - } - } - ItemDefinitionInner::Enum(ed) => { - self.add_doc_imports(&scope, &ed.doc, &mut links); - for v in &ed.variants { - self.add_doc_imports(&scope, &v.doc, &mut links); - } - for f in &ed.associated_functions { - self.add_doc_imports(&scope, &f.doc, &mut links); - } - } - ItemDefinitionInner::Bitflags(bd) => { - self.add_doc_imports(&scope, &bd.doc, &mut links); - for f in &bd.flags { - self.add_doc_imports(&scope, &f.doc, &mut links); - } - } - ItemDefinitionInner::TypeAlias(ta) => { - self.add_doc_imports(&scope, &ta.doc, &mut links); - } - ItemDefinitionInner::Constant(cd) => { - self.add_doc_imports(&scope, &cd.doc, &mut links); - } - ItemDefinitionInner::ExternValue(ev) => { - self.add_doc_imports(&scope, &ev.doc, &mut links); - } - } - } - - links - } - - fn add_doc_imports(&self, scope: &[ItemPath], doc: &[String], links: &mut ModuleDocLinks) { - for text in extract_links(doc) { - if let Some(target) = self.resolve(scope, &text) { - links.imports.insert(target.import_path()); - if let Some(value_path) = target.extern_value_path() { - links.extern_value_links.push((text, value_path)); - } - } - } - } - fn find_member(&self, item_path: &ItemPath, member: &str) -> Option { match &self.items.get(item_path)?.members { ItemMembers::Type { @@ -603,87 +584,201 @@ impl DocLinkResolver { } } -/// Validate every doc comment's intra-doc links, erroring on the first that -/// doesn't resolve. -pub fn validate( +/// Resolve every intra-doc link in every doc comment across the crate, in one +/// pass. Returns the per-module location-keyed link tables that backends +/// consume, or the first link that fails to resolve as an error. +/// +/// This is the single point where doc links are resolved — validation is this +/// pass's failure path, and every downstream consumer (backends, LSP hover on +/// build results) reads the returned tables rather than re-resolving with +/// locally-reconstructed context. +pub fn resolve_all( resolver: &DocLinkResolver, type_registry: &TypeRegistry, modules: &BTreeMap, -) -> Result<()> { - let check = |doc: &[String], scope: &[ItemPath], location: &ItemLocation| -> Result<()> { - for link in extract_links(doc) { - if resolver.resolve(scope, &link).is_none() { +) -> Result { + let mut links: DocLinks = modules + .keys() + .map(|k| (k.clone(), ModuleDocLinks::default())) + .collect(); + + // `key` identifies the doc block in the output table; `location` anchors + // any resolution error (for the module's own doc block, its proxy + // location — see [`DocBlockKey`]). + let mut record = |module_path: &ItemPath, + doc: &[String], + scope: &[ItemPath], + enclosing: Option<&ItemPath>, + key: DocBlockKey, + location: &ItemLocation| + -> Result<()> { + for (text, path) in extract_links(doc) { + let Some(target) = resolver.resolve(scope, &path, enclosing) else { return Err(SemanticError::DocLinkNotFound { - path: link, + path: text, location: *location, }); - } + }; + links + .entry(module_path.clone()) + .or_default() + .by_block + .entry(key) + .or_default() + .push(ResolvedDocLink { text, target }); } Ok(()) }; + let scopes: BTreeMap<&ItemPath, Vec> = modules + .iter() + .map(|(path, module)| (path, module.scope())) + .collect(); - for (path, module) in modules { - let scope = module.scope(); - check(module.doc(), &scope, module.location())?; + for (module_path, module) in modules { + let scope = &scopes[module_path]; + record( + module_path, + module.doc(), + scope, + None, + DocBlockKey::Module, + module.location(), + )?; for f in module.functions() { - check(&f.doc, &scope, &f.location)?; + record( + module_path, + &f.doc, + scope, + None, + DocBlockKey::Node(f.location), + &f.location, + )?; } - // Extern values' docs are checked in the registry walk below. - let _ = path; } + // Top-level items — those whose parent is a module. Items nested inside + // another item are reached by `walk_item_docs`' recursion instead, with + // the enclosing type's augmented scope. for (path, item) in type_registry.iter() { - let Some(resolved) = item.resolved() else { + let Some(parent) = path.parent() else { continue; }; - let module_path = path.parent().unwrap_or_else(ItemPath::empty); - let Some(module) = modules.get(&module_path) else { + let Some(scope) = scopes.get(&parent) else { continue; }; - let scope = module.scope(); - let loc = &item.location; - match &resolved.inner { - ItemDefinitionInner::Type(td) => { - check(&td.doc, &scope, loc)?; - for r in &td.regions { - check(&r.doc, &scope, &r.location)?; - } - for f in &td.associated_functions { - check(&f.doc, &scope, &f.location)?; - } - if let Some(v) = &td.vftable { - for f in &v.functions { - check(&f.doc, &scope, &f.location)?; - } - } + walk_item_docs( + type_registry, + &parent, + path, + item, + scope, + None, + &mut |module_path, doc, scope, enclosing, location| { + record( + module_path, + doc, + scope, + enclosing, + DocBlockKey::Node(*location), + location, + ) + }, + )?; + } + + Ok(links) +} + +/// Walk the doc comments of one item — its own doc, its members' docs, and +/// (recursively) any nested items' — calling `record` with the scope and +/// `Self`-enclosing context each doc block resolves under. +/// +/// `enclosing_type` for a doc block is the item whose *emitted* docs will +/// contain it: the type/enum/bitflags itself for its own, its members', and +/// its associated functions' docs, and the parent type for a nested +/// constant/extern value (the Rust backend emits those inside the parent's +/// `impl` block, where rustdoc resolves `Self` as the parent). Type aliases +/// and module-level constants/extern values get `None` — `Self` has nothing +/// to refer to in their emitted docs. +/// +/// `parent_type` is the type this item is nested inside, `None` at module +/// level. +fn walk_item_docs( + type_registry: &TypeRegistry, + module_path: &ItemPath, + path: &ItemPath, + item: &crate::semantic::types::ItemDefinition, + scope: &[ItemPath], + parent_type: Option<&ItemPath>, + record: &mut F, +) -> Result<()> +where + F: FnMut(&ItemPath, &[String], &[ItemPath], Option<&ItemPath>, &ItemLocation) -> Result<()>, +{ + let Some(resolved) = item.resolved() else { + return Ok(()); + }; + let enclosing = Some(path); + match &resolved.inner { + ItemDefinitionInner::Type(td) => { + // Augment scope with the type's own path so bare references to + // nested items (e.g. [InnerEnum]) resolve. + let type_scope: Vec = std::iter::once(path.clone()) + .chain(scope.iter().cloned()) + .collect(); + record(module_path, &td.doc, &type_scope, enclosing, &item.location)?; + for r in &td.regions { + record(module_path, &r.doc, &type_scope, enclosing, &r.location)?; } - ItemDefinitionInner::Enum(ed) => { - check(&ed.doc, &scope, loc)?; - for v in &ed.variants { - check(&v.doc, &scope, &v.location)?; - } - for f in &ed.associated_functions { - check(&f.doc, &scope, &f.location)?; - } + for f in &td.associated_functions { + record(module_path, &f.doc, &type_scope, enclosing, &f.location)?; } - ItemDefinitionInner::Bitflags(bd) => { - check(&bd.doc, &scope, loc)?; - for f in &bd.flags { - check(&f.doc, &scope, &f.location)?; + if let Some(v) = &td.vftable { + for f in &v.functions { + record(module_path, &f.doc, &type_scope, enclosing, &f.location)?; } } - ItemDefinitionInner::TypeAlias(ta) => { - check(&ta.doc, &scope, loc)?; + for nested_path in &td.nested_item_paths { + let Ok(nested_item) = type_registry.get(nested_path, &ItemLocation::internal()) + else { + continue; + }; + walk_item_docs( + type_registry, + module_path, + nested_path, + nested_item, + &type_scope, + Some(path), + record, + )?; } - ItemDefinitionInner::Constant(cd) => { - check(&cd.doc, &scope, loc)?; + } + ItemDefinitionInner::Enum(ed) => { + record(module_path, &ed.doc, scope, enclosing, &item.location)?; + for v in &ed.variants { + record(module_path, &v.doc, scope, enclosing, &v.location)?; } - ItemDefinitionInner::ExternValue(ev) => { - check(&ev.doc, &scope, loc)?; + for f in &ed.associated_functions { + record(module_path, &f.doc, scope, enclosing, &f.location)?; } } + ItemDefinitionInner::Bitflags(bd) => { + record(module_path, &bd.doc, scope, enclosing, &item.location)?; + for f in &bd.flags { + record(module_path, &f.doc, scope, enclosing, &f.location)?; + } + } + ItemDefinitionInner::TypeAlias(ta) => { + record(module_path, &ta.doc, scope, None, &item.location)?; + } + ItemDefinitionInner::Constant(cd) => { + record(module_path, &cd.doc, scope, parent_type, &item.location)?; + } + ItemDefinitionInner::ExternValue(ev) => { + record(module_path, &ev.doc, scope, parent_type, &item.location)?; + } } - Ok(()) } @@ -851,15 +946,22 @@ pub fn scan_links(text: &str) -> Vec { out } -/// Extract the intra-doc link path strings the compiler validates and imports: +/// Extract the intra-doc link paths the compiler validates and imports: /// the inline (`[label](path)`) and code-shortcut (`` [`path`] ``) forms. Bare /// `[path]` shortcuts are intentionally excluded. -pub fn extract_links(doc: &[String]) -> Vec { +/// +/// Returns `(original_text, parsed_path)` for each link — the original path +/// text exactly as written (what a backend substitutes when rewriting) and its +/// parsed [`DocLinkPath`] (what [`DocLinkResolver::resolve`] consumes). +pub fn extract_links(doc: &[String]) -> Vec<(String, DocLinkPath)> { let text = doc.join("\n"); scan_links(&text) .into_iter() .filter(|l| l.syntax != DocLinkSyntax::PlainShortcut) - .map(|l| l.path) + .map(|l| { + let parsed = DocLinkPath::parse(&l.path); + (l.path, parsed) + }) .collect() } diff --git a/src/semantic/ir.rs b/src/semantic/ir.rs index eb9fc60a..b11bf90a 100644 --- a/src/semantic/ir.rs +++ b/src/semantic/ir.rs @@ -90,6 +90,8 @@ pub struct SemanticAnalysis<'db> { #[returns(ref)] pub doc_link_resolver: Arc, #[returns(ref)] + pub doc_links: Arc, + #[returns(ref)] pub errors: Arc>, #[returns(ref)] pub parse_errors: Arc>, @@ -113,6 +115,7 @@ impl SemanticAnalysis<'_> { (**self.type_registry(db)).clone(), (**self.modules(db)).clone(), (**self.doc_link_resolver(db)).clone(), + (**self.doc_links(db)).clone(), )) } } diff --git a/src/semantic/output.rs b/src/semantic/output.rs index 669bf585..f07378da 100644 --- a/src/semantic/output.rs +++ b/src/semantic/output.rs @@ -9,16 +9,21 @@ use std::collections::BTreeMap; use crate::{ grammar::ItemPath, - semantic::{Module, doc_links::DocLinkResolver, type_registry::TypeRegistry}, + semantic::{ + Module, + doc_links::{DocLinkResolver, DocLinks, ModuleDocLinks}, + type_registry::TypeRegistry, + }, }; /// The output of semantic analysis, projected from `SemanticAnalysis`. -/// Backends take `&SemanticOutput` and read its three fields. +/// Backends take `&SemanticOutput` and read its fields. #[derive(Debug)] pub struct SemanticOutput { type_registry: TypeRegistry, modules: BTreeMap, doc_link_resolver: DocLinkResolver, + doc_links: DocLinks, } impl SemanticOutput { @@ -34,17 +39,34 @@ impl SemanticOutput { &self.doc_link_resolver } + /// The resolved intra-doc links of every module, produced once during + /// semantic analysis. Backends read link targets from here rather than + /// re-resolving doc text. + pub fn doc_links(&self) -> &DocLinks { + &self.doc_links + } + + /// The resolved doc links of one module. Every module present in + /// [`Self::modules`] has an entry (possibly empty). + pub fn module_doc_links(&self, module_path: &ItemPath) -> &ModuleDocLinks { + static EMPTY: std::sync::LazyLock = + std::sync::LazyLock::new(ModuleDocLinks::default); + self.doc_links.get(module_path).unwrap_or(&EMPTY) + } + /// Construct a `SemanticOutput` from its parts. /// Used by the Salsa query layer to project `SemanticAnalysis`. pub(crate) fn from_parts( type_registry: TypeRegistry, modules: BTreeMap, doc_link_resolver: DocLinkResolver, + doc_links: DocLinks, ) -> Self { Self { type_registry, modules, doc_link_resolver, + doc_links, } } } diff --git a/src/semantic/queries/root.rs b/src/semantic/queries/root.rs index 1e992349..b4390a0e 100644 --- a/src/semantic/queries/root.rs +++ b/src/semantic/queries/root.rs @@ -67,6 +67,7 @@ pub fn analyze<'db>( Arc::new(type_registry.clone()), Arc::new(modules.clone()), Arc::new(doc_link_resolver), + Arc::new(doc_links::DocLinks::new()), Arc::new(errors), Arc::new(vec![]), ) @@ -78,6 +79,7 @@ pub fn analyze<'db>( let finish = |type_registry: TypeRegistry, modules: BTreeMap, doc_link_resolver: doc_links::DocLinkResolver, + doc_links: doc_links::DocLinks, errors: Vec| -> SemanticAnalysis<'db> { SemanticAnalysis::new( @@ -85,6 +87,7 @@ pub fn analyze<'db>( Arc::new(type_registry), Arc::new(modules), Arc::new(doc_link_resolver), + Arc::new(doc_links), Arc::new(errors), Arc::new(vec![]), ) @@ -114,6 +117,7 @@ pub fn analyze<'db>( &TypeRegistry::new(pointer_size), &BTreeMap::new(), )), + Arc::new(doc_links::DocLinks::new()), Arc::new(vec![]), Arc::new(parse_errors), ); @@ -364,16 +368,29 @@ pub fn analyze<'db>( let af_errors = merge_associated_functions(&mut type_registry, &modules); if !af_errors.is_empty() { let doc_link_resolver = doc_links::DocLinkResolver::build(&type_registry, &modules); - return finish(type_registry, modules, doc_link_resolver, af_errors); + return finish( + type_registry, + modules, + doc_link_resolver, + doc_links::DocLinks::new(), + af_errors, + ); } - // Doc link resolution (now with associated functions in the registry) + // Doc link resolution (now with associated functions in the registry). + // This is the single pass that resolves every intra-doc link; validation + // is its failure path, and the resulting tables are what backends consume. let doc_link_resolver = doc_links::DocLinkResolver::build(&type_registry, &modules); - if let Err(e) = doc_links::validate(&doc_link_resolver, &type_registry, &modules) { - return finish(type_registry, modules, doc_link_resolver, vec![e]); + match doc_links::resolve_all(&doc_link_resolver, &type_registry, &modules) { + Ok(resolved) => finish(type_registry, modules, doc_link_resolver, resolved, vec![]), + Err(e) => finish( + type_registry, + modules, + doc_link_resolver, + doc_links::DocLinks::new(), + vec![e], + ), } - - finish(type_registry, modules, doc_link_resolver, vec![]) } /// Compute associated functions (own impl methods + inherited from base types) diff --git a/src/semantic/tests/doc_links.rs b/src/semantic/tests/doc_links.rs index 2bae88cf..0b9d6697 100644 --- a/src/semantic/tests/doc_links.rs +++ b/src/semantic/tests/doc_links.rs @@ -11,6 +11,11 @@ use crate::{ use super::util::*; use pretty_assertions::assert_eq; +/// Parse a written link path for `resolve()`. +fn segs(s: &str) -> crate::semantic::doc_links::DocLinkPath { + crate::semantic::doc_links::DocLinkPath::parse(s) +} + #[test] fn extracts_shortcut_and_inline_links() { let doc = vec![ @@ -19,10 +24,11 @@ fn extracts_shortcut_and_inline_links() { " A code-labelled inline link [`Update`](Mode::Update) too.".to_string(), " And plain [text] is ignored.".to_string(), ]; - assert_eq!( - extract_links(&doc), - vec!["Foo", "Bar::baz", "Qux::quux", "Mode::Update"] - ); + let texts: Vec = extract_links(&doc) + .into_iter() + .map(|(text, _)| text) + .collect(); + assert_eq!(texts, vec!["Foo", "Bar::baz", "Qux::quux", "Mode::Update"]); } #[test] @@ -30,7 +36,11 @@ fn ignores_brackets_inside_code_spans() { // `[first, last)` is a code span — its `[` must not consume the `]` // from the real link [`Target`]. let doc = vec![" Half-open range `[first, last)`: walks the [`Target`] list.".to_string()]; - assert_eq!(extract_links(&doc), vec!["Target"]); + let texts: Vec = extract_links(&doc) + .into_iter() + .map(|(text, _)| text) + .collect(); + assert_eq!(texts, vec!["Target"]); } #[test] @@ -78,41 +88,44 @@ fn resolves_every_link_form() { }; assert_eq!( - resolver.resolve(&scope, "Target"), + resolver.resolve(&scope, &segs("Target"), None), Some(DocLinkTarget::Item(IP::from("test::Target"))) ); assert_eq!( - resolver.resolve(&scope, "Target::do_it"), + resolver.resolve(&scope, &segs("Target::do_it"), None), Some(member("test::Target", "do_it", DocLinkMemberKind::Method)) ); assert_eq!( - resolver.resolve(&scope, "Target::m_value"), + resolver.resolve(&scope, &segs("Target::m_value"), None), Some(member("test::Target", "m_value", DocLinkMemberKind::Field)) ); assert_eq!( - resolver.resolve(&scope, "Mode::VarA"), + resolver.resolve(&scope, &segs("Mode::VarA"), None), Some(member("test::Mode", "VarA", DocLinkMemberKind::Variant)) ); assert_eq!( - resolver.resolve(&scope, "Flags::FlagX"), + resolver.resolve(&scope, &segs("Flags::FlagX"), None), Some(member("test::Flags", "FlagX", DocLinkMemberKind::Flag)) ); assert_eq!( - resolver.resolve(&scope, "helper"), + resolver.resolve(&scope, &segs("helper"), None), Some(DocLinkTarget::Function { module: IP::from("test"), name: "helper".to_string(), }) ); assert_eq!( - resolver.resolve(&scope, "global"), + resolver.resolve(&scope, &segs("global"), None), Some(DocLinkTarget::ExternValue { module: IP::from("test"), name: "global".to_string(), }) ); - assert_eq!(resolver.resolve(&scope, "Nonexistent"), None); - assert_eq!(resolver.resolve(&scope, "Target::missing"), None); + assert_eq!(resolver.resolve(&scope, &segs("Nonexistent"), None), None); + assert_eq!( + resolver.resolve(&scope, &segs("Target::missing"), None), + None + ); } #[test] @@ -139,7 +152,7 @@ fn resolves_nested_constant_as_member() { let resolver = state.doc_link_resolver(); assert_eq!( - resolver.resolve(&scope, "Player::STARTING_GOLD"), + resolver.resolve(&scope, &segs("Player::STARTING_GOLD"), None), Some(DocLinkTarget::Member { item: IP::from("test::Player"), name: "STARTING_GOLD".to_string(), @@ -148,8 +161,11 @@ fn resolves_nested_constant_as_member() { ); // A nested constant has no freestanding path to link to by bare name, so it // must not resolve as an item (which would emit an unresolvable import). - assert_eq!(resolver.resolve(&scope, "STARTING_GOLD"), None); - assert_eq!(resolver.resolve(&scope, "Player::MISSING"), None); + assert_eq!(resolver.resolve(&scope, &segs("STARTING_GOLD"), None), None); + assert_eq!( + resolver.resolve(&scope, &segs("Player::MISSING"), None), + None + ); } #[test] @@ -205,7 +221,9 @@ fn resolves_a_type_in_a_sibling_module() { .unwrap() .scope(); assert_eq!( - state.doc_link_resolver().resolve(&scope, "Other"), + state + .doc_link_resolver() + .resolve(&scope, &segs("Other"), None), Some(DocLinkTarget::Item(IP::from("other::Other"))) ); } @@ -222,3 +240,521 @@ fn errors_on_unresolved_doc_link() { "unexpected error: {err:?}" ); } + +// --- Self:: resolution tests --- + +#[test] +fn resolves_self_member() { + // A type with a field and method. `Self::field` and `Self::method` resolve + // as members of the enclosing type. + let module = + M::new() + .with_definitions([ID::new( + (V::Public, "Widget"), + TD::new([TS::field((V::Public, "m_value"), T::ident("u32")) + .with_attributes([A::address(0)])]) + .with_attributes([A::size(4), A::align(4)]), + )]) + .with_impls([FB::new( + "Widget", + [F::new((V::Public, "do_it"), [Ar::const_self()]) + .with_attributes([A::address(0x10)])], + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let scope = state.modules().get(&IP::from("test")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + let enclosing = IP::from("test::Widget"); + + assert_eq!( + resolver.resolve(&scope, &segs("Self::m_value"), Some(&enclosing)), + Some(DocLinkTarget::Member { + item: IP::from("test::Widget"), + name: "m_value".to_string(), + kind: DocLinkMemberKind::Field, + }) + ); + assert_eq!( + resolver.resolve(&scope, &segs("Self::do_it"), Some(&enclosing)), + Some(DocLinkTarget::Member { + item: IP::from("test::Widget"), + name: "do_it".to_string(), + kind: DocLinkMemberKind::Method, + }) + ); +} + +#[test] +fn resolves_self_member_without_enclosing_type() { + // `Self::member` at module scope (no enclosing type) returns None. + let module = M::new().with_definitions([ID::new( + (V::Public, "Widget"), + TD::new([ + TS::field((V::Public, "m_value"), T::ident("u32")).with_attributes([A::address(0)]) + ]) + .with_attributes([A::size(4), A::align(4)]), + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let scope = state.modules().get(&IP::from("test")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + + assert_eq!(resolver.resolve(&scope, &segs("Self::m_value"), None), None); +} + +#[test] +fn resolves_self_item() { + // Bare `Self` resolves to the enclosing type as a whole item. + let module = M::new().with_definitions([ID::new( + (V::Public, "Widget"), + TD::new([ + TS::field((V::Public, "m_value"), T::ident("u32")).with_attributes([A::address(0)]) + ]) + .with_attributes([A::size(4), A::align(4)]), + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let scope = state.modules().get(&IP::from("test")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + let enclosing = IP::from("test::Widget"); + + assert_eq!( + resolver.resolve(&scope, &segs("Self"), Some(&enclosing)), + Some(DocLinkTarget::Item(IP::from("test::Widget"))) + ); + // `Self` with no enclosing type returns None. + assert_eq!(resolver.resolve(&scope, &segs("Self"), None), None); +} + +#[test] +fn resolves_self_nested_type_member() { + // A type with a nested type that has a member. `Self::NestedType::member` + // resolves as a member of the nested type. + let module = M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([ + TS::item(ID::new( + (V::Public, "Inner"), + TD::new([TS::field((V::Public, "inner_field"), T::ident("u32")) + .with_attributes([A::address(0)])]) + .with_attributes([A::size(4), A::align(4)]), + )), + TS::field((V::Public, "outer_field"), T::ident("u32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(8), A::align(4)]), + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let scope = state.modules().get(&IP::from("test")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + let enclosing = IP::from("test::Outer"); + + assert_eq!( + resolver.resolve(&scope, &segs("Self::Inner::inner_field"), Some(&enclosing)), + Some(DocLinkTarget::Member { + item: IP::from("test::Outer::Inner"), + name: "inner_field".to_string(), + kind: DocLinkMemberKind::Field, + }) + ); +} + +// --- Module-qualified function/extern-value tests --- + +#[test] +fn resolves_qualified_function() { + // A function in module `a` referenced from module `b` via `a::func`. + let module_a = M::new().with_functions([ + F::new((V::Public, "shared_func"), []).with_attributes([A::address(0x10)]) + ]); + let module_b = M::new(); + + let mut builder = SemanticBuilder::new(pointer_size()); + builder.add_module(&module_a, &IP::from("a")).unwrap(); + builder.add_module(&module_b, &IP::from("b")).unwrap(); + let state = builder.build().unwrap(); + + let scope = state.modules().get(&IP::from("b")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + + assert_eq!( + resolver.resolve(&scope, &segs("a::shared_func"), None), + Some(DocLinkTarget::Function { + module: IP::from("a"), + name: "shared_func".to_string(), + }) + ); +} + +#[test] +fn resolves_qualified_extern_value() { + // An extern value in module `a` referenced from module `b` via `a::global`. + let module_a = M::new().with_definitions([ID::new( + (V::Public, "global"), + EVD::new(T::ident("u32").mut_pointer()).with_attributes([A::address(0x20)]), + )]); + let module_b = M::new(); + + let mut builder = SemanticBuilder::new(pointer_size()); + builder.add_module(&module_a, &IP::from("a")).unwrap(); + builder.add_module(&module_b, &IP::from("b")).unwrap(); + let state = builder.build().unwrap(); + + let scope = state.modules().get(&IP::from("b")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + + assert_eq!( + resolver.resolve(&scope, &segs("a::global"), None), + Some(DocLinkTarget::ExternValue { + module: IP::from("a"), + name: "global".to_string(), + }) + ); +} + +#[test] +fn resolves_qualified_function_not_found() { + // Module doesn't exist or function doesn't exist in that module. + let module_a = M::new().with_functions([ + F::new((V::Public, "shared_func"), []).with_attributes([A::address(0x10)]) + ]); + let module_b = M::new(); + + let mut builder = SemanticBuilder::new(pointer_size()); + builder.add_module(&module_a, &IP::from("a")).unwrap(); + builder.add_module(&module_b, &IP::from("b")).unwrap(); + let state = builder.build().unwrap(); + + let scope = state.modules().get(&IP::from("b")).unwrap().scope(); + let resolver = state.doc_link_resolver(); + + // Nonexistent module + assert_eq!( + resolver.resolve(&scope, &segs("nonexistent::func"), None), + None + ); + // Existing module, nonexistent function + assert_eq!( + resolver.resolve(&scope, &segs("a::missing_func"), None), + None + ); +} + +// --- validate() Self:: tests --- + +#[test] +fn validate_accepts_self_doc_link() { + // A type with a valid `Self::` doc-link in its doc comment should pass + // validation without error. + let module = + M::new() + .with_definitions([ID::new( + (V::Public, "Widget"), + TD::new([TS::field((V::Public, "m_value"), T::ident("u32")) + .with_attributes([A::address(0)])]) + .with_attributes([A::size(4), A::align(4)]), + ) + .with_doc_comments(vec![" See [`Self::m_value`] for the value.".to_string()])]) + .with_impls([FB::new( + "Widget", + [F::new((V::Public, "do_it"), [Ar::const_self()]) + .with_attributes([A::address(0x10)])], + )]); + + // Should succeed — no DocLinkNotFound error. + build_state(&module, &IP::from("test")).unwrap(); +} + +#[test] +fn validate_accepts_self_in_nested_item() { + // A type with a nested enum whose variant has a `Self::` doc-link should + // pass validation. + let module = M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([ + TS::item(ID::new( + (V::Public, "Inner"), + ED::new( + T::ident("u32"), + [ES::field("VariantA") + .with_doc_comments(vec![" See [`Self::VariantA`].".to_string()])], + [], + ), + )), + TS::field((V::Public, "outer_field"), T::ident("u32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(8), A::align(4)]), + )]); + + // Should succeed — `Self::VariantA` inside the nested enum refers to + // the nested enum's own variant. + build_state(&module, &IP::from("test")).unwrap(); +} + +#[test] +fn validate_rejects_invalid_link_in_nested_item() { + // A type with a nested enum whose variant has an invalid doc-link should + // fail validation. + let module = M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([ + TS::item(ID::new( + (V::Public, "Inner"), + ED::new( + T::ident("u32"), + [ES::field("VariantA") + .with_doc_comments(vec![" See [`Nonexistent`].".to_string()])], + [], + ), + )), + TS::field((V::Public, "outer_field"), T::ident("u32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(8), A::align(4)]), + )]); + + let err = build_state(&module, &IP::from("test")).unwrap_err(); + assert!( + matches!(&err, SemanticError::DocLinkNotFound { path, .. } if path == "Nonexistent"), + "unexpected error: {err:?}" + ); +} + +// --- DocLinkPath parsing --- + +#[test] +fn parses_doc_link_paths() { + use crate::{grammar::ItemPathSegment, semantic::doc_links::DocLinkPath}; + let seg = ItemPathSegment::from; + + assert_eq!( + DocLinkPath::parse("Foo"), + DocLinkPath { + self_prefixed: false, + segments: vec![seg("Foo")], + } + ); + assert_eq!( + DocLinkPath::parse("a::b::c"), + DocLinkPath { + self_prefixed: false, + segments: vec![seg("a"), seg("b"), seg("c")], + } + ); + assert_eq!( + DocLinkPath::parse("Self"), + DocLinkPath { + self_prefixed: true, + segments: vec![], + } + ); + assert_eq!( + DocLinkPath::parse("Self::member"), + DocLinkPath { + self_prefixed: true, + segments: vec![seg("member")], + } + ); + // `Self` only counts as a prefix in leading position. + assert_eq!( + DocLinkPath::parse("Foo::Self"), + DocLinkPath { + self_prefixed: false, + segments: vec![seg("Foo"), seg("Self")], + } + ); +} + +// --- Enclosing-type semantics of the doc walk --- + +#[test] +fn resolves_self_in_nested_constant_doc_as_parent() { + // A constant nested in a type emits as an associated const inside the + // parent's `impl` block, where rustdoc resolves `Self` as the parent type — + // so `Self::health` in its doc must resolve to the parent's field. + let module = M::new().with_definitions([ID::new( + (V::Public, "Player"), + TD::new([ + TS::item( + ID::new( + (V::Public, "STARTING_GOLD"), + CD::new(T::ident("u32"), int_literal(500)), + ) + .with_doc_comments(vec![" Initial value of [`Self::health`].".to_string()]), + ), + TS::field((V::Public, "health"), T::ident("i32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(4), A::align(4)]), + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let module_links = state.module_doc_links(&IP::from("test")); + let link = module_links + .iter() + .find(|l| l.text == "Self::health") + .expect("Self::health link recorded"); + assert_eq!( + link.target, + DocLinkTarget::Member { + item: IP::from("test::Player"), + name: "health".to_string(), + kind: DocLinkMemberKind::Field, + } + ); +} + +#[test] +fn validate_rejects_self_on_module_level_constant() { + // A module-level constant's emitted docs have no `Self` to refer to, so a + // `Self` link in them must fail validation rather than silently producing + // a link rustdoc can't resolve. + let module = M::new().with_definitions([ID::new( + (V::Public, "MAX_PLAYERS"), + CD::new(T::ident("u32"), int_literal(8)), + ) + .with_doc_comments(vec![" See [`Self`].".to_string()])]); + + let err = build_state(&module, &IP::from("test")).unwrap_err(); + assert!( + matches!(&err, SemanticError::DocLinkNotFound { path, .. } if path == "Self"), + "unexpected error: {err:?}" + ); +} + +#[test] +fn validate_rejects_self_on_type_alias() { + let module = + M::new().with_definitions([ID::new((V::Public, "Handle"), TAD::new(T::ident("u32"))) + .with_doc_comments(vec![" See [`Self`].".to_string()])]); + + let err = build_state(&module, &IP::from("test")).unwrap_err(); + assert!( + matches!(&err, SemanticError::DocLinkNotFound { path, .. } if path == "Self"), + "unexpected error: {err:?}" + ); +} + +#[test] +fn validate_rejects_invalid_link_in_nested_type_field_doc() { + // Docs on a *nested type's* fields are part of the walk too — a bad link + // there must fail validation just like one on a top-level type's field. + let module = M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([ + TS::item(ID::new( + (V::Public, "Inner"), + TD::new([TS::field((V::Public, "inner_field"), T::ident("u32")) + .with_attributes([A::address(0)]) + .with_doc_comments(vec![" See [`Nonexistent`].".to_string()])]) + .with_attributes([A::size(4), A::align(4)]), + )), + TS::field((V::Public, "outer_field"), T::ident("u32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(8), A::align(4)]), + )]); + + let err = build_state(&module, &IP::from("test")).unwrap_err(); + assert!( + matches!(&err, SemanticError::DocLinkNotFound { path, .. } if path == "Nonexistent"), + "unexpected error: {err:?}" + ); +} + +#[test] +fn resolves_self_in_nested_type_field_doc() { + // `Self::` in a nested type's field doc refers to the nested type itself. + let module = M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([ + TS::item(ID::new( + (V::Public, "Inner"), + TD::new([ + TS::field((V::Public, "first"), T::ident("u32")) + .with_attributes([A::address(0)]) + .with_doc_comments(vec![" Pairs with [`Self::second`].".to_string()]), + TS::field((V::Public, "second"), T::ident("u32")) + .with_attributes([A::address(4)]), + ]) + .with_attributes([A::size(8), A::align(4)]), + )), + TS::field((V::Public, "outer_field"), T::ident("u32")).with_attributes([A::address(0)]), + ]) + .with_attributes([A::size(12), A::align(4)]), + )]); + + let state = build_state(&module, &IP::from("test")).unwrap(); + let link = state + .module_doc_links(&IP::from("test")) + .iter() + .find(|l| l.text == "Self::second") + .expect("Self::second link recorded"); + assert_eq!( + link.target, + DocLinkTarget::Member { + item: IP::from("test::Outer::Inner"), + name: "second".to_string(), + kind: DocLinkMemberKind::Field, + } + ); +} + +#[test] +fn prefers_same_module_candidate_from_type_scoped_docs() { + // Two modules each define a type named `Context`; only module `b`'s has + // the field. A doc on a member of another type in `b` resolves under the + // *augmented* type scope (the type's own path is prepended for nested-item + // references), so the resolver must still anchor same-module preference at + // the module — not at whatever sits first in the scope list. Regression: + // the alphabetically-first `a::Context` (no such field) was picked and the + // link failed to resolve. + let module_a = M::new().with_definitions([ID::new( + (V::Public, "Context"), + TD::new([ + TS::field((V::Public, "unrelated"), T::ident("u32")).with_attributes([A::address(0)]) + ]) + .with_attributes([A::size(4), A::align(4)]), + )]); + let module_b = M::new() + .with_definitions([ + ID::new( + (V::Public, "Context"), + TD::new([TS::field((V::Public, "latch"), T::ident("u32")) + .with_attributes([A::address(0)])]) + .with_attributes([A::size(4), A::align(4)]), + ), + ID::new( + (V::Public, "Handle"), + TD::new([TS::field((V::Public, "data"), T::ident("u32")) + .with_attributes([A::address(0)])]) + .with_attributes([A::size(4), A::align(4)]), + ), + ]) + .with_impls([FB::new( + "Handle", + [F::new((V::Public, "consume"), [Ar::mut_self()]) + .with_attributes([A::address(0x10)]) + .with_doc_comments(vec![ + " Short-circuits on [`latch`](Context::latch).".to_string(), + ])], + )]); + + let mut builder = SemanticBuilder::new(pointer_size()); + builder.add_module(&module_a, &IP::from("a")).unwrap(); + builder.add_module(&module_b, &IP::from("b")).unwrap(); + let state = builder + .build() + .expect("Context::latch resolves to b::Context"); + + let link = state + .module_doc_links(&IP::from("b")) + .iter() + .find(|l| l.text == "Context::latch") + .expect("link recorded"); + assert_eq!( + link.target, + DocLinkTarget::Member { + item: IP::from("b::Context"), + name: "latch".to_string(), + kind: DocLinkMemberKind::Field, + } + ); +} diff --git a/src/span/mod.rs b/src/span/mod.rs index 8c1a5f13..d74ff269 100644 --- a/src/span/mod.rs +++ b/src/span/mod.rs @@ -31,7 +31,7 @@ impl std::fmt::Display for Location { } /// A span representing a range in source code -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Span { /// Start location (inclusive) pub start: Location, @@ -123,7 +123,7 @@ impl std::fmt::Display for FileId { /// Location of an item in source code (file ID + span) /// Every grammar and semantic item should have an ItemLocation for error reporting -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ItemLocation { /// Source file containing the item pub file_id: FileId, diff --git a/test.py b/test.py index 01f22fd6..7e2fbda3 100644 --- a/test.py +++ b/test.py @@ -5,8 +5,10 @@ """ import os +import shutil import sys import subprocess +import tempfile def run_command(cmd, env=None, cwd=None, shell=False): @@ -24,6 +26,63 @@ def run_command(cmd, env=None, cwd=None, shell=False): return result +def check_cpp_docs(): + """Run doxygen over codegen_tests/output/cpp and fail on any warning. + + The emitted headers carry doxygen-markdown doc links (`[label](@ref + ns::Target)`); an unresolvable `@ref` surfaces as a doxygen warning, so + warnings are treated as errors. `EXTRACT_ALL` keeps undocumented items + from warning — only genuine doc problems remain. + """ + print(f"\n{'=' * 60}") + print("Running: doxygen (C++ doc-link check on codegen_tests/output/cpp)") + print(f"{'=' * 60}\n") + + cpp_dir = os.path.abspath(os.path.join("codegen_tests", "output", "cpp")) + with tempfile.TemporaryDirectory(prefix="pyxis-doxygen-") as tmp: + warn_log = os.path.join(tmp, "warnings.log") + doxyfile = os.path.join(tmp, "Doxyfile") + with open(doxyfile, "w") as f: + f.write( + "\n".join( + [ + "PROJECT_NAME = pyxis-codegen-tests", + f"OUTPUT_DIRECTORY = {tmp}", + f"INPUT = {os.path.join(cpp_dir, 'include')}", + "FILE_PATTERNS = *.hpp", + "RECURSIVE = YES", + "EXTRACT_ALL = YES", + "GENERATE_HTML = YES", + "GENERATE_LATEX = NO", + "QUIET = YES", + "WARNINGS = YES", + "WARN_IF_UNDOCUMENTED = NO", + "WARN_IF_DOC_ERROR = YES", + f"WARN_LOGFILE = {warn_log}", + # Pin dot ON so every environment behaves the same: + # distro doxygen packages disagree on the default + # (Ubuntu's assumes dot exists, nix's assumes it + # doesn't), and half-enabled graph generation fails on + # its own map files. Graphviz comes from shell.nix + # locally and apt on CI, alongside doxygen itself. + "HAVE_DOT = YES", + ] + ) + + "\n" + ) + result = subprocess.run(["doxygen", doxyfile]) + warnings = "" + if os.path.exists(warn_log): + with open(warn_log) as f: + warnings = f.read().strip() + if result.returncode != 0 or warnings: + if warnings: + print(warnings) + print("\n[FAIL] doxygen reported problems in the emitted C++ docs") + sys.exit(1) + print("\n[PASS] Command succeeded") + + def main(): # Get command line arguments for cargo test test_args = sys.argv[1:] @@ -76,6 +135,19 @@ def main(): doc_env["RUSTDOCFLAGS"] = "-Dwarnings" run_command(["cargo", "doc", "--no-deps", "-p", "codegen_tests"], env=doc_env) + # Run doxygen over the emitted C++ corpus (catches unresolved `@ref`s in + # the rewritten doc links — the C++ analogue of the cargo doc gate above). + # The output is generated into a temp dir and discarded; only the warnings + # matter. Skipped with a warning when doxygen isn't installed (it's in + # shell.nix, and CI installs it). + if shutil.which("doxygen"): + check_cpp_docs() + else: + print(f"\n{'=' * 60}") + print("[SKIP] doxygen not found on PATH — C++ doc-link check skipped.") + print(" Install doxygen (see shell.nix) to run it locally.") + print(f"{'=' * 60}\n") + # Lint the viewer (tsc + eslint + prettier). This assumes the workspace # deps are already installed — CI runs `npm ci` before test.py, and local # devs are expected to `npm install` themselves. `shell=True` on Windows so diff --git a/tooling/lsp/src/handlers/doc_links.rs b/tooling/lsp/src/handlers/doc_links.rs index 9ab6b808..6569e116 100644 --- a/tooling/lsp/src/handlers/doc_links.rs +++ b/tooling/lsp/src/handlers/doc_links.rs @@ -1,6 +1,96 @@ use super::*; -use pyxis::semantic::doc_links::{DocLinkSyntax, DocLinkTarget, ScannedLink}; +use pyxis::semantic::doc_links::{DocLinkPath, DocLinkSyntax, DocLinkTarget, ScannedLink}; + +impl ServerState { + /// The `Self`-enclosing item for a doc-comment line — the type/enum/ + /// bitflags whose definition or `impl` block contains the line. Mirrors + /// the compiler's doc walk (`doc_links::resolve_all`): nested constants + /// and extern values use their *parent* type (their emitted docs land in + /// the parent's impl block), type aliases and module-level docs get + /// `None`. + pub(crate) fn enclosing_type_for_doc_line( + &self, + uri: &Uri, + line_no: usize, + ) -> Option { + let doc = self.documents.get(uri)?; + let parsed = semantic::parse_file(&self.db, doc.source_file); + let module = parsed.module(&self.db); + let own_module = self.module_path_for(uri); + let item_path = |name: &str| match &own_module { + Some(mp) => mp.join(name.into()), + None => ItemPath::from(name), + }; + + for item in &module.items { + match item { + ModuleItem::Definition { definition } + if location_contains_line(&definition.location, line_no) => + { + return enclosing_in_definition( + definition, + &item_path(definition.name.as_str()), + None, + line_no, + ); + } + ModuleItem::Impl { impl_block } + if location_contains_line(&impl_block.location, line_no) => + { + // `impl Outer::Inner` targets a nested item; join every + // written segment onto the module path. + let mut path = item_path(impl_block.name.as_str()); + if let Some(np) = &impl_block.name_path { + for seg in np.iter() { + path = path.join(seg.clone()); + } + } + return Some(path); + } + _ => {} + } + } + None + } +} + +fn location_contains_line(location: &pyxis::span::ItemLocation, line_no: usize) -> bool { + location.span.start.line <= line_no && line_no <= location.span.end.line +} + +/// Walk into a definition to find the enclosing item for `line_no`, mirroring +/// the compiler's enclosing-type semantics. `parent_type` is the type this +/// definition is nested inside (`None` at module level). +fn enclosing_in_definition( + definition: &ItemDefinition, + path: &ItemPath, + parent_type: Option<&ItemPath>, + line_no: usize, +) -> Option { + use pyxis::grammar::ItemDefinitionInner as IDI; + match &definition.inner { + IDI::Type(td) => { + // A line inside a nested item's own span belongs to that item. + for statement in td.statements() { + if let TypeField::Item(nested) = &statement.field + && location_contains_line(&nested.location, line_no) + { + return enclosing_in_definition( + nested, + &path.join(nested.name.as_str().into()), + Some(path), + line_no, + ); + } + } + Some(path.clone()) + } + IDI::Enum(_) | IDI::Bitflags(_) => Some(path.clone()), + IDI::Constant(_) | IDI::ExternValue(_) => parent_type.cloned(), + IDI::TypeAlias(_) => None, + } +} impl ServerState { /// Doc-comment links that reference `symbol`, returning the span of the @@ -64,9 +154,10 @@ impl ServerState { let Some(line) = lines.get(line_no - 1) else { continue; }; + let enclosing = self.enclosing_type_for_doc_line(uri, line_no); for dl in scan_doc_links(line) { if !resolver - .resolve(&scope, &dl.path) + .resolve(&scope, &DocLinkPath::parse(&dl.path), enclosing.as_ref()) .is_some_and(|t| matches(&t)) { continue; @@ -151,40 +242,43 @@ impl ServerState { let Some(line) = lines.get(line_no - 1) else { continue; }; + let enclosing = self.enclosing_type_for_doc_line(uri, line_no); for dl in scan_doc_links(line) { // Target file + 1-based line to anchor the link at, and a tooltip. - let Some((target_uri, target_line, tooltip)) = - (match resolver.resolve(&scope, &dl.path) { - Some(DocLinkTarget::Item(p)) => self - .resolved_definition(&p, type_registry, uri) - .map(|rd| (rd.uri, rd.name_span.start.line, p.to_string())), - Some(DocLinkTarget::Member { item, name, .. }) => self - .resolve_doc_member(&item, &name, uri) - .map(|(muri, mspan, _)| { - let path = item.join(name.as_str().into()); - (muri, mspan.start.line, path.to_string()) - }) - // Fall back to the owning type if the member's own - // declaration can't be located. - .or_else(|| { - self.resolved_definition(&item, type_registry, uri) - .map(|rd| (rd.uri, rd.name_span.start.line, item.to_string())) - }), - Some(DocLinkTarget::Function { module, name }) => self - .resolve_doc_module_item(&module, &name, uri, true) - .map(|(loc, _)| { - let path = module.join(name.as_str().into()); - (loc.uri, loc.range.start.line as usize + 1, path.to_string()) - }), - Some(DocLinkTarget::ExternValue { module, name }) => self - .resolve_doc_module_item(&module, &name, uri, false) - .map(|(loc, _)| { - let path = module.join(name.as_str().into()); - (loc.uri, loc.range.start.line as usize + 1, path.to_string()) - }), - None => None, - }) - else { + let Some((target_uri, target_line, tooltip)) = (match resolver.resolve( + &scope, + &DocLinkPath::parse(&dl.path), + enclosing.as_ref(), + ) { + Some(DocLinkTarget::Item(p)) => self + .resolved_definition(&p, type_registry, uri) + .map(|rd| (rd.uri, rd.name_span.start.line, p.to_string())), + Some(DocLinkTarget::Member { item, name, .. }) => self + .resolve_doc_member(&item, &name, uri) + .map(|(muri, mspan, _)| { + let path = item.join(name.as_str().into()); + (muri, mspan.start.line, path.to_string()) + }) + // Fall back to the owning type if the member's own + // declaration can't be located. + .or_else(|| { + self.resolved_definition(&item, type_registry, uri) + .map(|rd| (rd.uri, rd.name_span.start.line, item.to_string())) + }), + Some(DocLinkTarget::Function { module, name }) => self + .resolve_doc_module_item(&module, &name, uri, true) + .map(|(loc, _)| { + let path = module.join(name.as_str().into()); + (loc.uri, loc.range.start.line as usize + 1, path.to_string()) + }), + Some(DocLinkTarget::ExternValue { module, name }) => self + .resolve_doc_module_item(&module, &name, uri, false) + .map(|(loc, _)| { + let path = module.join(name.as_str().into()); + (loc.uri, loc.range.start.line as usize + 1, path.to_string()) + }), + None => None, + }) else { continue; }; let span = Span::new( @@ -433,6 +527,7 @@ impl ServerState { )) }; + let enclosing = self.enclosing_type_for_doc_line(uri, loc.line); for dl in scan_doc_links(line) { if col < dl.link.0 || col >= dl.link.1 { continue; @@ -441,7 +536,11 @@ impl ServerState { Location::new(loc.line, dl.link.0 + 1), Location::new(loc.line, dl.link.1 + 1), ); - let (location, hover) = match resolver.resolve(&scope, &dl.path)? { + let (location, hover) = match resolver.resolve( + &scope, + &DocLinkPath::parse(&dl.path), + enclosing.as_ref(), + )? { DocLinkTarget::Item(p) => to_type(self, &p)?, DocLinkTarget::Member { item, name, .. } => { match self.resolve_doc_member(&item, &name, uri) { diff --git a/tooling/lsp/tests/structures_test.rs b/tooling/lsp/tests/structures_test.rs index fcb7d416..7412e27c 100644 --- a/tooling/lsp/tests/structures_test.rs +++ b/tooling/lsp/tests/structures_test.rs @@ -1498,3 +1498,29 @@ fn rename_type_updates_splice_for_clause() { "rename should update def + splice `for` clause; got {total}" ); } + +#[test] +fn doc_links_resolve_self_prefixed_paths() { + // `Self::` doc links (issue #114) resolve against the enclosing type in + // the editor too: hover and go-to-definition work on a field doc's + // [`Self::member`] link, and inside an impl block's method docs. + let src = "pub type Foo {\n /// Pairs with [`Self::y`].\n pub x: u64,\n pub y: u64,\n}\nimpl Foo {\n /// Uses [`Self::x`].\n #[address(0x10)]\n pub fn go(&mut self);\n}\n"; + let st = ServerState::in_memory(&[("/p", 8, &[("m.pyxis", src)])]); + let uri = ServerState::document_uri("/p", "m.pyxis"); + + let field_col = src.lines().nth(1).unwrap().find("Self::y").unwrap() as u32 + 1; + assert!( + hover_text(&st, &uri, 1, field_col).contains("`y`"), + "hover on [`Self::y`] in a field doc resolves to the sibling field" + ); + assert!( + def_uri(&st, &uri, 1, field_col).is_some(), + "go-to-def on [`Self::y`] resolves" + ); + + let impl_col = src.lines().nth(6).unwrap().find("Self::x").unwrap() as u32 + 1; + assert!( + hover_text(&st, &uri, 6, impl_col).contains("`x`"), + "hover on [`Self::x`] in an impl method doc resolves to the field" + ); +}