Coverage for tsfpga/vhdl_file_documentation.py: 98%

50 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-10 20:51 +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 """ 

24 Methods to extract documentation from a VHDL source file. 

25 """ 

26 

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

28 """ 

29 Arguments: 

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

31 """ 

32 self._vhd_file_path = vhd_file_path 

33 

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

35 """ 

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

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

38 

39 Return: 

40 File header content. 

41 """ 

42 file_contents = read_file(self._vhd_file_path) 

43 

44 documentation_header_regexp = re.compile( 

45 VHDL_COMMENT_SEPARATOR 

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

47 + VHDL_COMMENT_SEPARATOR 

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

49 + VHDL_COMMENT_SEPARATOR 

50 + r"\n\n", 

51 re.DOTALL, 

52 ) 

53 match = documentation_header_regexp.search(file_contents) 

54 if match is None: 

55 return None 

56 

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

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

59 text = "" 

60 for line in lines: 

61 if line == "--": 

62 text += "\n" 

63 else: 

64 # Remove initial "-- " from comments 

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

66 

67 return text 

68 

69 def get_symbolator_component(self) -> Optional[str]: 

70 """ 

71 Return a string with a ``component`` declaration equivalent to the ``entity`` declaration 

72 within the file. (We use entity's but symbolator requires component's). 

73 

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

75 to handle them. 

76 

77 This implementation uses some regular expressions to find the generics and ports and modify 

78 them. 

79 The use of regular expressions makes it somewhat simple but also limited. 

80 Comments in strange places, specifically the string ``port (`` in a comment will make the 

81 mechanism fail. 

82 Also an entity with generics but no ports will be falsely interpreted as only ports. 

83 

84 These known limitations do not pose any known practical problem and are hence considered 

85 worth it in order to keep the implementation simple. 

86 The real solution would be to fix upstream in symbolator and hdlparse. 

87 

88 Return: 

89 VHDL ``component`` declaration. ``None`` if file is a package, and hence contains 

90 no ``entity``. 

91 """ 

92 if self._vhd_file_path.name.endswith("_pkg.vhd"): 

93 # File is a package, hence contains no entity 

94 return None 

95 

96 entity_name = self._vhd_file_path.stem 

97 entity_regexp = re.compile( 

98 rf"entity\s+{entity_name}\s+is" 

99 # Match all the code for generics and ports. 

100 # Is non-greedy, so it will only match up until the "end" declaration below. 

101 # Generic block optionally 

102 r"\s*(.+?)\s*(\)\s*;\s*)?" 

103 # Port block 

104 r"port\s*\(\s*(.+?)\s*" 

105 # 

106 r"\)\s*;\s*" 

107 # Shall be able to handle 

108 # end entity; 

109 # end entity <name>; 

110 # end <name>; 

111 # end; 

112 # with different whitespace around. 

113 rf"end(\s+entity|\s+entity\s+{entity_name}|\s+{entity_name}|)\s*;", 

114 re.IGNORECASE | re.DOTALL, 

115 ) 

116 

117 file_contents = read_file(self._vhd_file_path) 

118 

119 match = entity_regexp.search(file_contents) 

120 if match is None: 

121 return None 

122 

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

124 # Entity declaration contains both generics and ports 

125 generics = match.group(1) 

126 else: 

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

128 generics = None 

129 

130 ports = match.group(3) 

131 

132 # Remove default values. 

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

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

135 

136 # Replace the assignment with only the ending character (";" or "") 

137 def default_value_replace(match): # type: ignore 

138 return match.group(1) 

139 

140 # Remove any vector range declarations in port list. 

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

142 vector_regexp = re.compile(r"\([^;\n]+\)", re.IGNORECASE) 

143 

144 if generics: 

145 generics = default_value_regexp.sub(repl=default_value_replace, string=generics) 

146 generics = vector_regexp.sub(repl="", string=generics) 

147 

148 ports = default_value_regexp.sub(repl=default_value_replace, string=ports) 

149 ports = vector_regexp.sub(repl="", string=ports) 

150 

151 if generics: 

152 generics_code = f" {generics}\n );\n" 

153 else: 

154 generics_code = "" 

155 

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

157 

158 component = f"""\ 

159component {entity_name} is 

160{generics_code}{ports_code} 

161end component;""" 

162 

163 return component