Coverage for tsfpga/vhdl_file_documentation.py: 93%
71 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-13 07:59 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-13 07:59 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import re
12from typing import TYPE_CHECKING
14from tsfpga.system_utils import read_file
16if TYPE_CHECKING:
17 from pathlib import Path
19SEPARATOR_LINE_LENGTH = 100
20VHDL_COMMENT_SEPARATOR = "-- " + ("-" * (SEPARATOR_LINE_LENGTH - 3))
23class VhdlFileDocumentation:
24 """
25 Methods to extract documentation from a VHDL source file.
26 """
28 def __init__(self, vhd_file_path: Path) -> None:
29 """
30 Arguments:
31 vhd_file_path: Path to the VHDL file which shall be documented.
32 """
33 self._vhd_file_path = vhd_file_path
35 def get_header_rst(self) -> str | None:
36 """
37 Get the contents of the VHDL file's header. This means everything that is in the comment
38 block at the start of the file, after the copyright notice.
40 Return:
41 File header content.
42 """
43 file_contents = read_file(self._vhd_file_path)
45 documentation_header_regexp = re.compile(
46 VHDL_COMMENT_SEPARATOR
47 + r"\n(.+?)\n"
48 + VHDL_COMMENT_SEPARATOR
49 + r"\n(.+?)\n"
50 + VHDL_COMMENT_SEPARATOR
51 + r"\n\n",
52 re.DOTALL,
53 )
54 match = documentation_header_regexp.search(file_contents)
55 if match is None:
56 return None
58 # The first group will match the copyright header. Second group is documentation.
59 lines = match.group(2).split("\n")
60 text = ""
61 for line in lines:
62 if line == "--":
63 text += "\n"
64 else:
65 # Remove initial "-- " from comments
66 text += f"{line[3:]}\n"
68 return text
70 def get_symbolator_component(self) -> str | None: # noqa: C901
71 """
72 Return a string with a ``component`` declaration equivalent to the ``entity`` declaration
73 within the file. (We use entity's but symbolator requires component's).
75 Default values and range declarations on ports are removed since symbolator does not seem
76 to handle them.
78 This implementation uses some regular expressions to find the generics and ports and modify
79 them.
80 The use of regular expressions makes it somewhat simple but also limited.
81 Comments in strange places, specifically the string ``port (`` in a comment will make the
82 mechanism fail.
83 Also an entity with generics but no ports will be falsely interpreted as only ports.
85 These known limitations do not pose any known practical problem and are hence considered
86 worth it in order to keep the implementation simple.
87 The real solution would be to fix upstream in symbolator and hdlparse.
89 Return:
90 VHDL ``component`` declaration.
91 ``None`` if file is a package, and hence contains no ``entity``.
92 ``None`` if no ``entity`` is found in the file.
93 """
94 if self._vhd_file_path.name.endswith("_pkg.vhd"):
95 # File is a package, hence contains no entity
96 return None
98 vhdl = read_file(self._vhd_file_path)
100 def replace_comment(match: re.Match) -> str:
101 match_group = match.group(1)
102 if (
103 match_group
104 and match_group.startswith("# {{")
105 and match_group.endswith("}}")
106 and ";" not in match_group
107 ):
108 # This is a valid symbolator comment, leave it as is.
109 # I.e. "--# {{}}" or "--# {{some text}}
110 return f"--{match_group}"
112 # Strip comment
113 return ""
115 # Remove comments so that the remaining VHDL is easier to parse.
116 vhdl = re.sub(pattern=r"--(.*)$", repl=replace_comment, string=vhdl, flags=re.MULTILINE)
118 # Strip trailing whitespace and empty lines.
119 vhdl = re.sub(pattern=r"\s*$", repl="", string=vhdl, flags=re.MULTILINE)
121 # Split out the entity declaration from the VHDL file.
122 entity_name = self._vhd_file_path.stem
123 entity_regexp = re.compile(
124 rf"entity\s+{entity_name}\s+is\s+"
125 # Match all the code for generics and ports.
126 # Is non-greedy, so it will only match up until the "end" declaration below.
127 r"(.*?)"
128 # Shall be able to handle
129 # end entity;
130 # end entity <name>;
131 # end <name>;
132 # end;
133 # with different whitespace around.
134 rf"\s*end(\s+entity|\s+entity\s+{entity_name}|\s+{entity_name}|)\s*;",
135 re.IGNORECASE | re.DOTALL,
136 )
138 match = entity_regexp.search(vhdl)
139 if match is None or not match.group(1):
140 print(f"Found no entity in {self._vhd_file_path}")
141 return None
143 ports_and_generics = match.group(1)
145 # Remove attribute lines within the entity declaration.
146 ports_and_generics = re.sub(
147 pattern=r"^\s*attribute\s+.+", repl="", string=ports_and_generics, flags=re.MULTILINE
148 )
150 # Slit out the generic part and the port part from the entity declaration.
151 ports_and_generics_regexp = re.compile(
152 # Match all the code for generics and ports.
153 # Is non-greedy, so it will only match up until the "end" declaration below.
154 # Generic block optionally
155 r"\s*(.+?)?\s*(\)\s*;\s*)?"
156 # Port block
157 r"port\s*\(\s*(.+?)\s*"
158 # Closing.
159 r"\)\s*;\s*$",
160 re.IGNORECASE | re.DOTALL,
161 )
163 match = ports_and_generics_regexp.search(ports_and_generics)
164 if match is None:
165 print(f"Found no ports or generics in {self._vhd_file_path}")
166 return None
168 if match.group(2) and match.group(3):
169 # Entity declaration contains both generics and ports
170 generics = match.group(1)
171 if not generics.lower().startswith("generic"):
172 raise ValueError("Expected start of generic block")
174 strip_generics_open = re.compile(r"generic\s*\(", re.IGNORECASE)
175 generics = strip_generics_open.sub(repl="", string=generics)
176 else:
177 # Only one match, which we assume is ports (generics only is not supported)
178 generics = None
180 ports = match.group(3)
182 # Remove default values.
183 # Symbolator stops parsing if it encounters vector default values (others => ...).
184 default_value_regexp = re.compile(r"\s*:=.+$", re.IGNORECASE | re.DOTALL)
186 # Remove any vector range declarations in port/generic list.
187 # The lines become too long so they don't fit in the image.
188 vector_regexp = re.compile(r"\(.*$", re.IGNORECASE | re.DOTALL)
190 def clean_up_declarations(declarations: str) -> str:
191 clean_declarations = []
193 # Split the list of declarations string into individual.
194 # Note that this fails if there are any ";" in comments, so its import that we
195 # strip comments before this.
196 for declaration in declarations.split(";"):
197 cleaned = default_value_regexp.sub(repl="", string=declaration)
198 cleaned = vector_regexp.sub(repl="", string=cleaned)
200 clean_declarations.append(cleaned)
202 return ";".join(clean_declarations)
204 if generics:
205 generics = clean_up_declarations(generics)
207 ports = clean_up_declarations(ports)
209 generics_code = f" generic ({generics}\n );\n" if generics else ""
211 ports_code = f" port (\n {ports}\n );"
213 return f"""\
214component {entity_name} is
215{generics_code}{ports_code}
216end component;"""