Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a80ecbb
Add support for doxygen parameters
matthewhayessssss Aug 18, 2025
0a4a4de
Add doxygen style metadata and add predoc for doxygen comments
matthewhayessssss Aug 21, 2025
e5dba45
Add new tests for featurs and fix old tests
matthewhayessssss Aug 21, 2025
17e86dc
Translated doxygen commands into ford commands
matthewhayessssss Aug 26, 2025
bce00f4
Translates doxy meta data to ford metadata
matthewhayessssss Aug 27, 2025
67d7428
Add Support for @see function
matthewhayessssss Aug 27, 2025
8b65d18
Add option for user to turn off doxygen support
matthewhayessssss Aug 27, 2025
773e594
Added documentation for doxygen changed
matthewhayessssss Aug 28, 2025
69f1d60
Merge branch 'master' into Doxygen
ZedThree Oct 20, 2025
9b099d5
Fix windows uris in tests
ZedThree Oct 20, 2025
0fcc399
tests: Remove unnecessary import
ZedThree Oct 20, 2025
1a78258
Apply ruff formatting
ZedThree Oct 20, 2025
73e1e13
Small rewrite of Doxygen docs section
ZedThree Oct 20, 2025
f90dde4
Small refactor of doxygen command parsing
ZedThree Oct 20, 2025
5b5812c
Fix Doxygen metadata translation (`details` -> `brief`)
ZedThree Oct 20, 2025
e199184
Allow multiple doxygen metadata commands in docstring
ZedThree Oct 20, 2025
c9b3e0d
Ruff format tests
ZedThree Oct 20, 2025
1550ab4
Small tidy of example
ZedThree Oct 20, 2025
045a17b
[skip ci] Apply black changes
ZedThree Oct 20, 2025
cf03e85
Fix for docstrings on source files
ZedThree Oct 20, 2025
7f250f7
Support direction in `@param` doxygen command
ZedThree Oct 20, 2025
3e088a3
Add test for `@see` doxygen command
ZedThree Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/user_guide/project_file_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,14 @@ displayed). These choice can be overridden for a specific item using the
`documentation meta data <metadata-display>`, and those settings will be
inherited by any items they contain. (*default:* ‘public’ and ‘protected’)

.. _option-doxygen:

doxygen
^^^^^^^

If true, attempt to parse Doxygen-style doc-comments (see
:ref:`doxygen-docs`). (*default:* ``True``)

.. _option-external:

external
Expand Down
45 changes: 45 additions & 0 deletions docs/user_guide/writing_documentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,48 @@ adding its name next to the comment symbol:
.. code:: text

extra_filetypes: inc ! fortran.FortranFixedLexer

.. _doxygen-docs:

Doxygen Documentation
------------------------

FORD now offers some limited support for `Doxygen <https://www.doxygen.nl>`_
syntax, including ``@param``, ``@see``, and some metadata such as ``@author``.

In the Doxygen format, procedure parameters are documented in the procedure
doc-comment using ``@param <arg-name> <description>``. While Doxygen also allows
supplying the argument direction (``[in]``, for example), Ford ignores this and
takes this information from the argument ``intent`` instead.

To link to another construct, Doxygen uses the ``@see`` command followed by the
name of the construct to be linked to and then the rest of the comment. In
Doxygen, the name of the link must come first, whereas in ford the name can come
anywhere in the comment

The other supported Doxygen commands are:

- ``@author``
- ``@date``
- ``@deprecated``
- ``@license``
- ``@version``

which translate directly to their Ford counterparts (see :ref:`sec-doc-metadata`).

You can also use ``@brief``, which is converted to ``@summary``.

If you encounter problems with parsing Doxygen comments, you can turn this off
with the :ref:`option-doxygen` setting in your project configuration file.

.. note:: Doxygen support is intended to be minimal! We don't intend to support
anything beyond a limited subset of Doxygen commands. Our parsing of
Doxygen commands is also limited, and we expect the command and its
argument to be on the same line. For example::

!> @author Ann Coder

and not::

!> @author
!> Ann Coder
1 change: 1 addition & 0 deletions example/src/ford_example_type.f90
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module ford_example_type_mod
!!
!! More documentation
contains
!> @see example_type this is a doxygen link
procedure :: say_hello => example_type_say
!! This will document the type-bound procedure *binding*, rather
!! than the procedure itself. The specific procedure docs will be
Expand Down
9 changes: 6 additions & 3 deletions example/src/ford_test_program.f90
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
!> This is a normal doxygen comment
!> @param global_pi This is a doxygen comment for global pi
program ford_test_program
!! Simple Fortran program to demonstrate the usage of FORD and to test its installation
use iso_fortran_env, only: output_unit, real64
Expand All @@ -19,19 +21,20 @@ program ford_test_program
call do_stuff(20)

contains
!> @brief This is a doxygen-style summary
!> Followed by the main body
subroutine do_foo_stuff(this)
use test_module, only: increment
class(foo) :: this
call increment(this%foo_stuff)
end subroutine do_foo_stuff

!> @deprecated true
!> @param repeat This is a doxygen comment for the name variable repeat
subroutine do_stuff(repeat)
!! source: True
!!
!! This is documentation for our subroutine that does stuff and things.
!! This text is captured by ford
integer, intent(in) :: repeat
!! The number of times to repeatedly do stuff and things
integer :: i
!! internal loop counter

Expand Down
2 changes: 1 addition & 1 deletion ford/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ def _make_graph_as_table(self):
try:
link = f'<a href="{attribs["URL"]}">{attribs["label"]}</a></td>'
except KeyError:
link = f'{attribs["label"]}</td>'
link = f"{attribs['label']}</td>"

node = f'<td rowspan="2" class="node" bgcolor="{attribs["color"]}">{link}'

Expand Down
1 change: 1 addition & 0 deletions ford/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class ProjectSettings:
display: List[str] = field(default_factory=lambda: ["public", "protected"])
doc_license: str = ""
docmark: str = "!"
doxygen: bool = True
docmark_alt: str = "*"
email: Optional[str] = None
encoding: str = "utf-8"
Expand Down
91 changes: 87 additions & 4 deletions ford/sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
from ford.console import warn
from ford.reader import FortranReader
import ford.utils
from ford.utils import paren_split, strip_paren
from ford.utils import DOXY_META_RE, paren_split, strip_paren
from ford.intrinsics import INTRINSICS
from ford._markdown import MetaMarkdown
from ford.settings import ProjectSettings, EntitySettings
Expand Down Expand Up @@ -103,6 +103,17 @@
"common": "common",
}

DOXYGEN_TRANSLATION = {"brief": "summary"}
DOXYGEN_PARAM_RE = re.compile(
r"""\s*@param\s* # Required command
(?:\[[inout ,]+\]\s*)? # Optional direction (not properly parsed here!)
(?P<name>[\S]*) # Required parameter name
(?P<comment>\s+.*) # Description of parameter
""",
re.VERBOSE,
)
DOXYGEN_SEE_RE = re.compile(r"\s*@see\s*(?P<name>\S+)\s*(?P<comment>[\S\s]*)?")


def _find_in_list(collection: Iterable, name: str) -> Optional[FortranBase]:
for item in collection:
Expand All @@ -126,6 +137,53 @@ def read_docstring(source: FortranReader, docmark: str) -> List[str]:
return docstring


def translate_links(arr: list[str]) -> list[str]:
"""Translates Doxygen ``@see`` links to Ford's ``[[]]``"""

for i, doc in enumerate(arr):
if match := DOXYGEN_SEE_RE.match(doc):
name = match["name"]
comment = f" {match['comment']}" or ""
arr[i] = f"[[{name}]]{comment}"
return arr


def create_doxy_dict(line: str) -> dict:
"""Create a dictionary of parameters with a name and comment"""

return {m["name"]: m["comment"] for m in DOXYGEN_PARAM_RE.finditer(line)}


def remove_doxy(source: list) -> List[str]:
"""Remove doxygen comments with an @ identifier from main comment block."""
return [line for line in source if not DOXYGEN_PARAM_RE.match(line)]


def translate_doxy_meta(doc_list: list[str]) -> list[str]:
"""Convert doxygen metadata into ford's format"""

# Doxygen commands can appear anywhere, we must move
# to the start of the docstring, and we can't do that
# while iterating on the list
to_be_moved: list[int] = []

for line, comment in enumerate(doc_list):
if match := DOXY_META_RE.match(comment):
meta_type = match["key"]
meta_content = match["value"].strip()
meta_type = DOXYGEN_TRANSLATION.get(meta_type, meta_type)
if meta_type != "param":
doc_list[line] = f"{meta_type}: {meta_content}"
to_be_moved.append(line)

for line in to_be_moved:
# This is fine because reorder earlier indices
# doesn't affect later ones
doc_list.insert(0, doc_list.pop(line))

return doc_list


class Associations:
"""
A class for storing associations. Associations are added and removed in batches, akin
Expand Down Expand Up @@ -226,6 +284,11 @@ def __init__(

self.base_url = pathlib.Path(self.settings.project_url)
self.doc_list = read_docstring(source, self.settings.docmark)
if self.settings.doxygen:
self.doc_list = translate_links(self.doc_list)

# For line that has been logged in the doc_list we need to check

self.hierarchy = self._make_hierarchy()
self.read_metadata()

Expand Down Expand Up @@ -397,6 +460,11 @@ def read_metadata(self):
self.meta = EntitySettings.from_project_settings(self.settings)

if len(self.doc_list) > 0:
# we must translate the doxygen metadata into the ford format
# @(meta) (content) ---> (meta): (content)
if self.settings.doxygen:
self.doc_list = translate_doxy_meta(self.doc_list)

if len(self.doc_list) == 1 and ":" in self.doc_list[0]:
words = self.doc_list[0].split(":")[0].strip()
field_names = [field.name for field in fields(EntitySettings)]
Expand Down Expand Up @@ -733,7 +801,12 @@ class FortranContainer(FortranBase):
)

def __init__(
self, source, first_line, parent=None, inherited_permission="public", strings=[]
self,
source,
first_line,
parent=None,
inherited_permission="public",
strings=[],
):
self.num_lines = 0
if not isinstance(self, FortranSourceFile):
Expand All @@ -752,6 +825,7 @@ def __init__(
self.VARIABLE_RE = re.compile(
self.VARIABLE_STRING.format(typestr), re.IGNORECASE
)
self.doxy_dict: Dict[str, str] = {}

# This is a little bit confusing, because `permission` here is sort of
# overloaded for "permission for this entity", and "permission for child
Expand All @@ -767,14 +841,19 @@ def __init__(

blocklevel = 0
associations = Associations()

for line in source:
if line[0:2] == "!" + self.settings.docmark:
self.doc_list.append(line[2:])
continue

if self.settings.doxygen:
# Parse doxygen commands and remove them from the docstring
for comment in self.doc_list:
self.doxy_dict.update(create_doxy_dict(comment))
self.doc_list = remove_doxy(self.doc_list)

if line.strip() != "":
self.num_lines += 1

# Temporarily replace all strings to make the parsing simpler
self.strings = []
search_from = 0
Expand Down Expand Up @@ -2991,6 +3070,10 @@ def line_to_variables(source, line, inherit_permission, parent):
)
search_from += QUOTES_RE.search(initial[search_from:]).end(0)

# If the parent has a doxygen `@param` command, add it to any Ford docstring
if doxy_doc := parent.doxy_dict.pop(name, None):
doc.append(doxy_doc)

varlist.append(
FortranVariable(
name,
Expand Down
6 changes: 6 additions & 0 deletions ford/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def traverse(root, attrs) -> list:

# Global Vars
META_RE = re.compile(r"^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)")
DOXY_META_RE = re.compile(r"^[ ]{0,3}@(?P<key>[A-Za-z0-9_-]+)\s+(?P<value>.*)")
META_MORE_RE = re.compile(r"^[ ]{4,}(?P<value>.*)")
BEGIN_RE = re.compile(r"^-{3}(\s.*)?")
END_RE = re.compile(r"^(-{3}|\.{3})(\s.*)?")
Expand Down Expand Up @@ -255,17 +256,22 @@ def meta_preprocessor(lines: Union[str, List[str]]) -> Tuple[Dict[str, Any], Lis
line = lines.pop(0)
if line.strip() == "" or END_RE.match(line):
break # blank line or end of YAML header - done

# Finds a FORD metadata match and classifies
if m1 := META_RE.match(line):
key = m1.group("key").lower().strip()
value = m1.group("value").strip()
meta[key].append(value)

else:
if (m2 := META_MORE_RE.match(line)) and key:
# Add another line to existing key
meta[key].append(m2.group("value").strip())

else:
lines.insert(0, line)
break # no meta data - done

return meta, lines


Expand Down
2 changes: 1 addition & 1 deletion test/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def test_variable_lists(example_project):

assert len(varlist("tr")) == 2
assert varlist.tbody.tr.find(class_="anchor")["id"] == "variable-global_pi"
expected_declaration = "real(kind=real64) :: global_pi = acos(-1) a global variable, initialized to the value of pi"
expected_declaration = "real(kind=real64) :: global_pi = acos(-1) a global variable, initialized to the value of pi This is a doxygen comment for global pi"
declaration_no_whitespace = varlist.tbody.text.replace("\n", "").replace(" ", "")
assert declaration_no_whitespace == expected_declaration.replace(" ", "")

Expand Down
1 change: 1 addition & 0 deletions test/test_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ def test_graphs(
assert legend.find_all("g", class_="edge") == []


@pytest.mark.skipif(not graphviz_installed, reason="Requires graphviz")
def test_external_module_links(make_project_graphs):
graphs = make_project_graphs

Expand Down
Loading