Coverage for tsfpga/vhdl_file_documentation.py: 96%

71 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-07 11:31 +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# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import re 

11from pathlib import Path 

12from typing import Optional 

13 

14# First party libraries 

15from tsfpga.system_utils import read_file 

16 

17SEPARATOR_LINE_LENGTH = 100 

18VHDL_COMMENT_SEPARATOR = "-- " + ("-" * (SEPARATOR_LINE_LENGTH - 3)) 

19 

20 

21class VhdlFileDocumentation: 

22 """ 

23 Methods to extract documentation from a VHDL source file. 

24 """ 

25 

26 def __init__(self, vhd_file_path: Path) -> None: 

27 """ 

28 Arguments: 

29 vhd_file_path: Path to the VHDL file which shall be documented. 

30 """ 

31 self._vhd_file_path = vhd_file_path 

32 

33 def get_header_rst(self) -> Optional[str]: 

34 """ 

35 Get the contents of the VHDL file's header. This means everything that is in the comment 

36 block at the start of the file, after the copyright notice. 

37 

38 Return: 

39 File header content. 

40 """ 

41 file_contents = read_file(self._vhd_file_path) 

42 

43 documentation_header_regexp = re.compile( 

44 VHDL_COMMENT_SEPARATOR 

45 + r"\n(.+?)\n" 

46 + VHDL_COMMENT_SEPARATOR 

47 + r"\n(.+?)\n" 

48 + VHDL_COMMENT_SEPARATOR 

49 + r"\n\n", 

50 re.DOTALL, 

51 ) 

52 match = documentation_header_regexp.search(file_contents) 

53 if match is None: 

54 return None 

55 

56 # The first group will match the copyright header. Second group is documentation. 

57 lines = match.group(2).split("\n") 

58 text = "" 

59 for line in lines: 

60 if line == "--": 

61 text += "\n" 

62 else: 

63 # Remove initial "-- " from comments 

64 text += f"{line[3:]}\n" 

65 

66 return text 

67 

68 def get_symbolator_component( # pylint: disable=too-many-locals,too-many-statements 

69 self, 

70 ) -> Optional[str]: 

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). 

74 

75 Default values and range declarations on ports are removed since symbolator does not seem 

76 to handle them. 

77 

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. 

84 

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. 

88 

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 

97 

98 vhdl = read_file(self._vhd_file_path) 

99 

100 def replace_comment(match): # type: ignore 

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}" 

111 

112 # Strip comment 

113 return "" 

114 

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) 

117 

118 # Strip trailing whitespace and empty lines. 

119 vhdl = re.sub(pattern=r"\s*$", repl="", string=vhdl, flags=re.MULTILINE) 

120 

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 ) 

137 

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 

142 

143 ports_and_generics = match.group(1) 

144 

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 ) 

149 

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 # 

159 r"\)\s*;\s*$", 

160 re.IGNORECASE | re.DOTALL, 

161 ) 

162 

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 

167 

168 if match.group(2) and match.group(3): 

169 # Entity declaration contains both generics and ports 

170 generics = match.group(1) 

171 assert generics.lower().startswith("generic") 

172 

173 strip_generics_open = re.compile(r"generic\s*\(", re.IGNORECASE) 

174 generics = strip_generics_open.sub(repl="", string=generics) 

175 else: 

176 # Only one match, which we assume is ports (generics only is not supported) 

177 generics = None 

178 

179 ports = match.group(3) 

180 

181 # Remove default values. 

182 # Symbolator stops parsing if it encounters vector default values (others => ...). 

183 default_value_regexp = re.compile(r"\s*:=.+$", re.IGNORECASE | re.DOTALL) 

184 

185 # Remove any vector range declarations in port/generic list. 

186 # The lines become too long so they don't fit in the image. 

187 vector_regexp = re.compile(r"\(.*$", re.IGNORECASE | re.DOTALL) 

188 

189 def clean_up_declarations(declarations: str) -> str: 

190 clean_declarations = [] 

191 

192 # Split the list of declarations string into individual. 

193 # Note that this fails if there are any ";" in comments, so its import that we 

194 # strip comments before this. 

195 for declaration in declarations.split(";"): 

196 cleaned = default_value_regexp.sub(repl="", string=declaration) 

197 cleaned = vector_regexp.sub(repl="", string=cleaned) 

198 

199 clean_declarations.append(cleaned) 

200 

201 return ";".join(clean_declarations) 

202 

203 if generics: 

204 generics = clean_up_declarations(generics) 

205 

206 ports = clean_up_declarations(ports) 

207 

208 if generics: 

209 generics_code = f" generic ({generics}\n );\n" 

210 else: 

211 generics_code = "" 

212 

213 ports_code = f" port (\n {ports}\n );" 

214 

215 component = f"""\ 

216component {entity_name} is 

217{generics_code}{ports_code} 

218end component;""" 

219 

220 return component