Coverage for tsfpga/vhdl_file_documentation.py: 98%

48 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-29 20:01 +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://gitlab.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import re 

11 

12# First party libraries 

13from tsfpga.system_utils import read_file 

14from tsfpga.test.lint.test_copyright import CopyrightHeader 

15 

16VHDL_COMMENT_SEPARATOR = "-- " + ("-" * (CopyrightHeader.separator_line_length - 3)) 

17 

18 

19class VhdlFileDocumentation: 

20 

21 """ 

22 Methods to extract documentation from a VHDL source file. 

23 """ 

24 

25 def __init__(self, vhd_file_path): 

26 """ 

27 Arguments: 

28 vhd_file_path (pathlib.Path): Path to the VHDL file which shall be documented. 

29 """ 

30 self._vhd_file_path = vhd_file_path 

31 

32 def get_header_rst(self): 

33 """ 

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

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

36 

37 Return: 

38 str: File header content. 

39 """ 

40 file_contents = read_file(self._vhd_file_path) 

41 

42 documentation_header_regexp = re.compile( 

43 VHDL_COMMENT_SEPARATOR 

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

45 + VHDL_COMMENT_SEPARATOR 

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

47 + VHDL_COMMENT_SEPARATOR 

48 + r"\n\n", 

49 re.DOTALL, 

50 ) 

51 match = documentation_header_regexp.search(file_contents) 

52 if match is None: 

53 return None 

54 

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

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

57 text = "" 

58 for line in lines: 

59 if line == "--": 

60 text += "\n" 

61 else: 

62 # Remove initial "-- " from comments 

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

64 

65 return text 

66 

67 def get_symbolator_component(self): 

68 """ 

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

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

71 

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

73 to handle them. 

74 

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

76 them. 

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

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

79 mechanism fail. 

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

81 

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

83 worth it in order to keep the implementation simple. 

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

85 

86 Return: 

87 str: VHDL ``component`` declaration. ``None`` if file is a package, and hence contains 

88 no ``entity``. 

89 """ 

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

91 # File is a package, hence contains no entity 

92 return None 

93 

94 entity_name = self._vhd_file_path.stem 

95 entity_regexp = re.compile( 

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

97 # Match all the code for generics and ports. 

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

99 # Generic block optionally 

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

101 # Port block 

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

103 # 

104 r"\)\s*;\s*" 

105 # Shall be able to handle 

106 # end entity; 

107 # end entity <name>; 

108 # end <name>; 

109 # end; 

110 # with different whitespace around. 

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

112 re.IGNORECASE | re.DOTALL, 

113 ) 

114 

115 file_contents = read_file(self._vhd_file_path) 

116 

117 match = entity_regexp.search(file_contents) 

118 if match is None: 

119 return None 

120 

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

122 # Entity declaration contains both generics and ports 

123 generics = match.group(1) 

124 else: 

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

126 generics = None 

127 

128 ports = match.group(3) 

129 

130 # Remove default values. 

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

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

133 

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

135 def default_value_replace(match): 

136 return match.group(1) 

137 

138 # Remove any vector range declarations in port list. 

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

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

141 

142 if generics: 

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

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

145 

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

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

148 

149 if generics: 

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

151 else: 

152 generics_code = "" 

153 

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

155 

156 component = f"""\ 

157component {entity_name} is 

158{generics_code}{ports_code} 

159end component;""" 

160 

161 return component