Skip to content

Commit 6d9f1e7

Browse files
authored
Merge pull request #711 from matthewhayessssss/Doxygen
Add basic parsing of Doxygen comments
2 parents 9c500a1 + 3e088a3 commit 6d9f1e7

File tree

11 files changed

+268
-10
lines changed

11 files changed

+268
-10
lines changed

docs/user_guide/project_file_options.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,14 @@ displayed). These choice can be overridden for a specific item using the
847847
`documentation meta data <metadata-display>`, and those settings will be
848848
inherited by any items they contain. (*default:* ‘public’ and ‘protected’)
849849

850+
.. _option-doxygen:
851+
852+
doxygen
853+
^^^^^^^
854+
855+
If true, attempt to parse Doxygen-style doc-comments (see
856+
:ref:`doxygen-docs`). (*default:* ``True``)
857+
850858
.. _option-external:
851859

852860
external

docs/user_guide/writing_documentation.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,48 @@ adding its name next to the comment symbol:
392392
.. code:: text
393393
394394
extra_filetypes: inc ! fortran.FortranFixedLexer
395+
396+
.. _doxygen-docs:
397+
398+
Doxygen Documentation
399+
------------------------
400+
401+
FORD now offers some limited support for `Doxygen <https://www.doxygen.nl>`_
402+
syntax, including ``@param``, ``@see``, and some metadata such as ``@author``.
403+
404+
In the Doxygen format, procedure parameters are documented in the procedure
405+
doc-comment using ``@param <arg-name> <description>``. While Doxygen also allows
406+
supplying the argument direction (``[in]``, for example), Ford ignores this and
407+
takes this information from the argument ``intent`` instead.
408+
409+
To link to another construct, Doxygen uses the ``@see`` command followed by the
410+
name of the construct to be linked to and then the rest of the comment. In
411+
Doxygen, the name of the link must come first, whereas in ford the name can come
412+
anywhere in the comment
413+
414+
The other supported Doxygen commands are:
415+
416+
- ``@author``
417+
- ``@date``
418+
- ``@deprecated``
419+
- ``@license``
420+
- ``@version``
421+
422+
which translate directly to their Ford counterparts (see :ref:`sec-doc-metadata`).
423+
424+
You can also use ``@brief``, which is converted to ``@summary``.
425+
426+
If you encounter problems with parsing Doxygen comments, you can turn this off
427+
with the :ref:`option-doxygen` setting in your project configuration file.
428+
429+
.. note:: Doxygen support is intended to be minimal! We don't intend to support
430+
anything beyond a limited subset of Doxygen commands. Our parsing of
431+
Doxygen commands is also limited, and we expect the command and its
432+
argument to be on the same line. For example::
433+
434+
!> @author Ann Coder
435+
436+
and not::
437+
438+
!> @author
439+
!> Ann Coder

example/src/ford_example_type.f90

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module ford_example_type_mod
2828
!!
2929
!! More documentation
3030
contains
31+
!> @see example_type this is a doxygen link
3132
procedure :: say_hello => example_type_say
3233
!! This will document the type-bound procedure *binding*, rather
3334
!! than the procedure itself. The specific procedure docs will be

example/src/ford_test_program.f90

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
!> This is a normal doxygen comment
2+
!> @param global_pi This is a doxygen comment for global pi
13
program ford_test_program
24
!! Simple Fortran program to demonstrate the usage of FORD and to test its installation
35
use iso_fortran_env, only: output_unit, real64
@@ -19,19 +21,20 @@ program ford_test_program
1921
call do_stuff(20)
2022

2123
contains
24+
!> @brief This is a doxygen-style summary
25+
!> Followed by the main body
2226
subroutine do_foo_stuff(this)
2327
use test_module, only: increment
2428
class(foo) :: this
2529
call increment(this%foo_stuff)
2630
end subroutine do_foo_stuff
2731

32+
!> @deprecated true
33+
!> @param repeat This is a doxygen comment for the name variable repeat
2834
subroutine do_stuff(repeat)
29-
!! source: True
30-
!!
3135
!! This is documentation for our subroutine that does stuff and things.
3236
!! This text is captured by ford
3337
integer, intent(in) :: repeat
34-
!! The number of times to repeatedly do stuff and things
3538
integer :: i
3639
!! internal loop counter
3740

ford/graphs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def _make_graph_as_table(self):
978978
try:
979979
link = f'<a href="{attribs["URL"]}">{attribs["label"]}</a></td>'
980980
except KeyError:
981-
link = f'{attribs["label"]}</td>'
981+
link = f"{attribs['label']}</td>"
982982

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

ford/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class ProjectSettings:
132132
display: List[str] = field(default_factory=lambda: ["public", "protected"])
133133
doc_license: str = ""
134134
docmark: str = "!"
135+
doxygen: bool = True
135136
docmark_alt: str = "*"
136137
email: Optional[str] = None
137138
encoding: str = "utf-8"

ford/sourceform.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
from ford.console import warn
5656
from ford.reader import FortranReader
5757
import ford.utils
58-
from ford.utils import paren_split, strip_paren
58+
from ford.utils import DOXY_META_RE, paren_split, strip_paren
5959
from ford.intrinsics import INTRINSICS
6060
from ford._markdown import MetaMarkdown
6161
from ford.settings import ProjectSettings, EntitySettings
@@ -103,6 +103,17 @@
103103
"common": "common",
104104
}
105105

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

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

128139

140+
def translate_links(arr: list[str]) -> list[str]:
141+
"""Translates Doxygen ``@see`` links to Ford's ``[[]]``"""
142+
143+
for i, doc in enumerate(arr):
144+
if match := DOXYGEN_SEE_RE.match(doc):
145+
name = match["name"]
146+
comment = f" {match['comment']}" or ""
147+
arr[i] = f"[[{name}]]{comment}"
148+
return arr
149+
150+
151+
def create_doxy_dict(line: str) -> dict:
152+
"""Create a dictionary of parameters with a name and comment"""
153+
154+
return {m["name"]: m["comment"] for m in DOXYGEN_PARAM_RE.finditer(line)}
155+
156+
157+
def remove_doxy(source: list) -> List[str]:
158+
"""Remove doxygen comments with an @ identifier from main comment block."""
159+
return [line for line in source if not DOXYGEN_PARAM_RE.match(line)]
160+
161+
162+
def translate_doxy_meta(doc_list: list[str]) -> list[str]:
163+
"""Convert doxygen metadata into ford's format"""
164+
165+
# Doxygen commands can appear anywhere, we must move
166+
# to the start of the docstring, and we can't do that
167+
# while iterating on the list
168+
to_be_moved: list[int] = []
169+
170+
for line, comment in enumerate(doc_list):
171+
if match := DOXY_META_RE.match(comment):
172+
meta_type = match["key"]
173+
meta_content = match["value"].strip()
174+
meta_type = DOXYGEN_TRANSLATION.get(meta_type, meta_type)
175+
if meta_type != "param":
176+
doc_list[line] = f"{meta_type}: {meta_content}"
177+
to_be_moved.append(line)
178+
179+
for line in to_be_moved:
180+
# This is fine because reorder earlier indices
181+
# doesn't affect later ones
182+
doc_list.insert(0, doc_list.pop(line))
183+
184+
return doc_list
185+
186+
129187
class Associations:
130188
"""
131189
A class for storing associations. Associations are added and removed in batches, akin
@@ -226,6 +284,11 @@ def __init__(
226284

227285
self.base_url = pathlib.Path(self.settings.project_url)
228286
self.doc_list = read_docstring(source, self.settings.docmark)
287+
if self.settings.doxygen:
288+
self.doc_list = translate_links(self.doc_list)
289+
290+
# For line that has been logged in the doc_list we need to check
291+
229292
self.hierarchy = self._make_hierarchy()
230293
self.read_metadata()
231294

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

399462
if len(self.doc_list) > 0:
463+
# we must translate the doxygen metadata into the ford format
464+
# @(meta) (content) ---> (meta): (content)
465+
if self.settings.doxygen:
466+
self.doc_list = translate_doxy_meta(self.doc_list)
467+
400468
if len(self.doc_list) == 1 and ":" in self.doc_list[0]:
401469
words = self.doc_list[0].split(":")[0].strip()
402470
field_names = [field.name for field in fields(EntitySettings)]
@@ -733,7 +801,12 @@ class FortranContainer(FortranBase):
733801
)
734802

735803
def __init__(
736-
self, source, first_line, parent=None, inherited_permission="public", strings=[]
804+
self,
805+
source,
806+
first_line,
807+
parent=None,
808+
inherited_permission="public",
809+
strings=[],
737810
):
738811
self.num_lines = 0
739812
if not isinstance(self, FortranSourceFile):
@@ -752,6 +825,7 @@ def __init__(
752825
self.VARIABLE_RE = re.compile(
753826
self.VARIABLE_STRING.format(typestr), re.IGNORECASE
754827
)
828+
self.doxy_dict: Dict[str, str] = {}
755829

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

768842
blocklevel = 0
769843
associations = Associations()
770-
771844
for line in source:
772845
if line[0:2] == "!" + self.settings.docmark:
773846
self.doc_list.append(line[2:])
774847
continue
848+
849+
if self.settings.doxygen:
850+
# Parse doxygen commands and remove them from the docstring
851+
for comment in self.doc_list:
852+
self.doxy_dict.update(create_doxy_dict(comment))
853+
self.doc_list = remove_doxy(self.doc_list)
854+
775855
if line.strip() != "":
776856
self.num_lines += 1
777-
778857
# Temporarily replace all strings to make the parsing simpler
779858
self.strings = []
780859
search_from = 0
@@ -2991,6 +3070,10 @@ def line_to_variables(source, line, inherit_permission, parent):
29913070
)
29923071
search_from += QUOTES_RE.search(initial[search_from:]).end(0)
29933072

3073+
# If the parent has a doxygen `@param` command, add it to any Ford docstring
3074+
if doxy_doc := parent.doxy_dict.pop(name, None):
3075+
doc.append(doxy_doc)
3076+
29943077
varlist.append(
29953078
FortranVariable(
29963079
name,

ford/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def traverse(root, attrs) -> list:
203203

204204
# Global Vars
205205
META_RE = re.compile(r"^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)")
206+
DOXY_META_RE = re.compile(r"^[ ]{0,3}@(?P<key>[A-Za-z0-9_-]+)\s+(?P<value>.*)")
206207
META_MORE_RE = re.compile(r"^[ ]{4,}(?P<value>.*)")
207208
BEGIN_RE = re.compile(r"^-{3}(\s.*)?")
208209
END_RE = re.compile(r"^(-{3}|\.{3})(\s.*)?")
@@ -255,17 +256,22 @@ def meta_preprocessor(lines: Union[str, List[str]]) -> Tuple[Dict[str, Any], Lis
255256
line = lines.pop(0)
256257
if line.strip() == "" or END_RE.match(line):
257258
break # blank line or end of YAML header - done
259+
260+
# Finds a FORD metadata match and classifies
258261
if m1 := META_RE.match(line):
259262
key = m1.group("key").lower().strip()
260263
value = m1.group("value").strip()
261264
meta[key].append(value)
265+
262266
else:
263267
if (m2 := META_MORE_RE.match(line)) and key:
264268
# Add another line to existing key
265269
meta[key].append(m2.group("value").strip())
270+
266271
else:
267272
lines.insert(0, line)
268273
break # no meta data - done
274+
269275
return meta, lines
270276

271277

test/test_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def test_variable_lists(example_project):
337337

338338
assert len(varlist("tr")) == 2
339339
assert varlist.tbody.tr.find(class_="anchor")["id"] == "variable-global_pi"
340-
expected_declaration = "real(kind=real64) :: global_pi = acos(-1) a global variable, initialized to the value of pi"
340+
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"
341341
declaration_no_whitespace = varlist.tbody.text.replace("\n", "").replace(" ", "")
342342
assert declaration_no_whitespace == expected_declaration.replace(" ", "")
343343

test/test_graphs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ def test_graphs(
464464
assert legend.find_all("g", class_="edge") == []
465465

466466

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

0 commit comments

Comments
 (0)