diff --git a/.github/scripts/check_guideline.py b/.github/scripts/check_guideline.py new file mode 100644 index 000000000..d56e5520a --- /dev/null +++ b/.github/scripts/check_guideline.py @@ -0,0 +1,1133 @@ +#!/usr/bin/env python3 + +############################################################################## +########################### HOW TO USE THIS SCRIPT ########################### +############################################################################## +## +## Check readme_check_guideline.md from the same folder, for more details. +## +############################################################################## + +import os +import re +import codecs +import sys +from datetime import datetime + +############################################################################## +# +# Class definitions +############################################################################## +class Port (object): + def __init__ (self, name="unknown", direction="unknown", ptype="wire"): + self.name = name + self.direction = direction + self.ptype = ptype + + +class Occurrence (object): + # path - to the file where the occurrence was found + # line - where the instantiated module is + # line_end - where the instantiated module ends + # pos_start_ports - how many lines after .line the ports list starts, inside + # the instantiated module + def __init__ (self, path="unknown", line="unknown"): + self.path = path + self.line = line + self.line_end = -1 + self.pos_start_ports = -1 + + +class Interface (object): + def __init__ (self): + self.interface = [] + + def add_port (self, port): + self.interface.append(port) + + +############################################################################## +# +# Functions +############################################################################## +def is_comment (line): + rcoma = re.compile(r'^\s*//') + rcomb = re.compile(r'^\s*/\*') + if (rcoma.match(line) or rcomb.match(line)): + return True + else: + return False + +def is_multiline_comment (line): + if ((line.strip()).startswith("*")): + return True + else: + if ((line.find("/*") != -1) or (line.find("*/") != -1)): + return True + else: + return False + +def is_paramdef (line): + rparameter = re.compile(r'^\s*parameter\s.*') + if (rparameter.match(line)): + return True + else: + return False + +def is_iodef (line): + rinput = re.compile(r'^\s*input\s.*') + routput= re.compile(r'^\s*output\s.*') + rinout = re.compile(r'^\s*inout\s.*') + if ((rinput.match(line)) or (routput.match(line)) or (rinout.match(line))): + return True + else: + return False + +# check if the given string is made only of spaces or tabs +def only_spaces_or_tabs (substr): + substr = substr.strip() + substr = substr.strip("\t") + if (substr == ""): + return True + else: + return False + + +# check if one of the modified files appears in the warning message +def list_has_substring (modified_files, message): + for mfile in modified_files: + if (message.find(mfile) != -1): + return True + + return False + + +# check if file is between the modified files specified as arguments +def string_in_list (module_path, modified_files): + for mfile_path in modified_files: + if (("./" + mfile_path) == module_path or mfile_path == module_path): + return True + + return False + + +############################################################################### +# +# Check if file has correct properties, meaning that the file extension has to +# be .v/.sv and it should not be some certain files. +# Returns true or false. +############################################################################### +def check_filename (filename): + + if (filename.endswith('.v') == False and filename.endswith('.sv') == False): + return False + if (filename.find("tb") != -1): + return False + + return True + + +############################################################################### +# +# Detect all modules present in the given directory in /library and /projects. +# Return a list with the relative paths. +############################################################################### +def detect_all_modules (directory): + + detected_modules_list = [] + for folder, dirs, files in os.walk(directory): + ## folder name must be either library or projects, + ## and it must not contain a dot in the name (Vivado generated) + if ((folder[1:-2]).find(".") == -1 + and (folder.find("library") != -1 or folder.find("projects") != -1)): + + for file in files: + #filename_wout_ext = (os.path.splitext(file)[0]) + if (check_filename(file)): + fullpath = os.path.join(folder, file) + detected_modules_list.append(fullpath) + + return detected_modules_list + + +############################################################################### +# +# Determine the file name from the fullpath. +# Return the string containing the file name without extension. +############################################################################### +def get_file_name (module_path): + + # split the path using the / and take the last group, which is the file.ext + split_path = module_path.split("/") + module_filename = split_path[len(split_path) - 1] + + # take the module name from the filename with the extension + filename_wout_ext = module_filename.split(".")[0] + + return filename_wout_ext + + +############################################################################### +# +# Check if there are lines after `endmodule and two consecutive empty lines, +# and if there are and edit_files is true, delete them. +############################################################################### +def check_extra_lines (module_path, list_of_lines, lw, edit_files): + + passed_endmodule = False + line_nb = 1 + prev_line = "" + remove_end_lines = False + + if (edit_files): + remove_extra_lines = False + + for line in list_of_lines: + # GC: check for lines after endmodule + if (line.find("endmodule") != -1): + passed_endmodule = True + + # if we passed the endmodule tag + if (passed_endmodule and (line.find("endmodule") == -1)): + remove_end_lines = True + + # GC: check for empty lines + if (line_nb >= 2): + if (only_spaces_or_tabs(prev_line) and only_spaces_or_tabs(line) + and (not is_comment(prev_line)) and (not is_comment(line))): + + lw.append(module_path + " : " + str(line_nb) + " two or more consecutive empty lines") + if (edit_files): + remove_extra_lines = True + line_nb += 1 + if (line_nb >= 2): + prev_line = line + + if (remove_end_lines): + if (edit_files): + deleted_lines = False + passed_endmodule = False + line_nb = 1 + + while (line_nb <= len(list_of_lines)): + line = list_of_lines[line_nb-1] + if (line.find("endmodule") != -1): + passed_endmodule = True + if (not (passed_endmodule and (line.find("endmodule") == -1))): + line_nb += 1 + else: + deleted_lines = True + list_of_lines.pop(line_nb-1) + + if (deleted_lines): + lw.append(module_path + " : deleted lines after endmodule") + else: + lw.append(module_path + " : couldn't delete lines after endmodule but must!") + else: + lw.append(module_path + " : extra lines after endmodule") + + if (edit_files and remove_extra_lines): + line_nb = 1 + prev_line = "" + while (line_nb <= len(list_of_lines)): + line = list_of_lines[line_nb-1] + if (line_nb >= 2): + if (only_spaces_or_tabs(prev_line) and only_spaces_or_tabs(line) + and (not is_comment(prev_line)) and (not is_comment(line))): + lw.append(module_path + " : " + str(line_nb) + " removed consecutive empty lines") + list_of_lines.pop(line_nb-1) + else: + line_nb += 1 + else: + line_nb += 1 + if (line_nb >= 2): + prev_line = line + + +############################################################################### +# Get the nth digit from a number. +# The numbering in this scheme uses zero-indexing and starts from the right side +# of the number. +# The // performs integer division by a power of ten to move the digit to the +# ones position, then the % gets the remainder after division by 10. +############################################################################### +def get_digit (number, n): + return number // 10**n % 10 + + +############################################################################### +# +# List of files that strings that the module path must not contain, in order to +# check for the license header. +############################################################################### +avoid_list = [] +avoid_list.append("jesd") +avoid_list.append("fir_interp") +avoid_list.append("cic_interp") + +def header_check_allowed (module_path): + + for str in avoid_list: + if (module_path.find(str) != -1): + return False + return True + + +############################################################################### +# +# Check if the license header is written correctly, meaning: +# To have either a range of years from the first time it was committed and +# until the current year +# or just the current year, if this is the first commit. +############################################################################### +def check_license (list_of_lines, lw, edit_files): + + currentYear = datetime.now().year + license_header = """// *************************************************************************** +// *************************************************************************** +// Copyright """ + str(currentYear) + """ (c) Analog Devices, Inc. All rights reserved. +// +// In this HDL repository, there are many different and unique modules, consisting +// of various HDL (Verilog or VHDL) components. The individual modules are +// developed independently, and may be accompanied by separate and unique license +// terms. +// +// The user should read each of these license terms, and understand the +// freedoms and responsibilities that he or she has by using this source/core. +// +// This core is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. +// +// Redistribution and use of source or resulting binaries, with or without modification +// of this file, are permitted under one of the following two license terms: +// +// 1. The GNU General Public License version 2 as published by the +// Free Software Foundation, which can be found in the top level directory +// of this repository (LICENSE_GPL2), and also online at: +// +// +// OR +// +// 2. An ADI specific BSD license, which can be found in the top level directory +// of this repository (LICENSE_ADIBSD), and also on-line at: +// https://github.com/analogdevicesinc/hdl/blob/master/LICENSE_ADIBSD +// This will allow to generate bit files and not release the source code, +// as long as it attaches to an ADI device. +// +// *************************************************************************** +// ***************************************************************************""" + + changed = False + template_matches = True + header_status = -1 + + # for further development, if the entire license should be checked + # number of lines for the license header text, including the last line + #lh_nb = license_header.count('\n') + 1 + + # if this is the line with the Copyright year + line_nb = 2 + ## from [13-23] is the range of years + ## or [13-16] is the year + aux = list(list_of_lines[line_nb]) + # match a year range + match = re.match(r'.*(Copyright\s20[0-9]{2}\s[-]\s20[0-9]{2}\s\(c\))', list_of_lines[line_nb]) + if (match is not None): + # only the last year must be updated (chars [20-23]) + c1 = str(get_digit(currentYear, 3)) + c2 = str(get_digit(currentYear, 2)) + c3 = str(get_digit(currentYear, 1)) + c4 = str(get_digit(currentYear, 0)) + + # if already set to current year, then no edits and no warnings + if (aux[20] == c1 and aux[21] == c2 and aux[22] == c3 and aux[23] == c4): + changed = False + else: + aux[20] = c1 + aux[21] = c2 + aux[22] = c3 + aux[23] = c4 + changed = True + else: + # match a single year + match = re.match(r'.*(Copyright\s20[0-9]{2}\s\(c\))', list_of_lines[line_nb]) + + if (match is not None): + ## if the year is different than the currentYear, + ## then must make a year range [13-23] + year = aux[13] + aux[14] + aux[15] + aux[16] + if (year != str(currentYear)): + aux.insert(17, ' ') + aux.insert(18, '-') + aux.insert(19, ' ') + aux.insert(20, str(get_digit(currentYear, 3))) + aux.insert(21, str(get_digit(currentYear, 2))) + aux.insert(22, str(get_digit(currentYear, 1))) + aux.insert(23, str(get_digit(currentYear, 0))) + changed = True + else: + changed = False + else: + # if none of the Copyright templates match + template_matches = False + + # files can be changed and header got updated + if (edit_files and changed and template_matches): + list_of_lines[line_nb] = "".join(aux) + lw.append(module_path + " : license header updated by the script") + header_status = 1 + + # header up-to-date already and matches a template + if (not changed and template_matches): + header_status = 2 + + # files cannot be updated and header is not up-to-date + if (not edit_files and changed and template_matches): + lw.append(module_path + " : license header is not updated") + header_status = 3 + + return header_status + + +############################################################################### +# +# Check for guideline rules applied to module definitions and the entire file, +# except for the module instances. They are processed in check_guideline_instances. +# This can modify the files if edit_files is true. +# Return the string containing the module name and print errors for guideline +# if it is not respected. +############################################################################### +def get_and_check_module (module_path, lw, edit_files): + + lw_initial_size = len(lw) + lw.append("\nAt module definition:") + + fp = open("%s" % (module_path), "r") + list_of_lines = fp.readlines() + fp.close() + + ## do not check the license status for the files that must be avoided, + ## since it doesn't apply + if (header_check_allowed(module_path)): + header_status = check_license(list_of_lines, lw, edit_files) + # GC: check if the license header is updated + if (header_status == -1): + edited = False + lw.append(module_path + " : license header doesn't match the pattern for the Copyright year") + else: + header_status = -1 + + module_name = "" + name_found = False + params_exist = False + end_line = -1 + line_nb = 1 + passed_module = False + passed_endmodule = False + last_iodef_line = -1 + last_paramdef_line = -1 + changed_line1 = -1 + changed_line2 = -1 + changed_line1_sit = -1 + extra_chars = False + + for line in list_of_lines: + pos_module = line.find("module") + pos_endmodule = line.find("endmodule") + pos_paranth1 = line.find("(") + pos_comma = line.find(",") + + if (pos_module == 0): + passed_module = True + + if (pos_endmodule != -1): + passed_endmodule = True + + # GC: check for spaces at the end of line + if (re.search(" +$", line) != None): + extra_chars = True + lw.append(module_path + " : " + str(line_nb) + " extra spaces at the end of line") + + # if the module declaration didn't end already + if (is_paramdef(line) and passed_module and end_line == -1): + if (pos_comma == -1): + last_paramdef_line = line_nb + else: + pos_comment = line.find("/") + ## if the first found comma is after a /, it means it's the + ## last parameter line + if (pos_comment > 0 and pos_comment < pos_comma): + last_paramdef_line = line_nb + + # if the module declaration didn't end already + if (is_iodef(line) and passed_module and end_line == -1): + if (pos_comma == -1): + last_iodef_line = line_nb + else: + pos_comment = line.find("/") + ## if the first found comma is after a /, it means it's the + ## last io line + if (pos_comment > 0 and pos_comment < pos_comma): + last_iodef_line = line_nb + + # if still inside the module declaration (with params) + if (name_found and params_exist and end_line == -1): + pos_paranth2 = line.find(")") + + if (0 <= pos_paranth2 and pos_paranth2 < pos_paranth1): + if (re.search("\)\\s\(", line) != None): + + rest_of_line = line.strip().strip("(").strip().strip(")") + ## GC: situations when the guideline is not respected: + ## 1. | ) ( + ## 2. |) ( something + ## 3. | smth ) ( something + ## 4. means it's one of the above + if (pos_paranth2 > 0 or rest_of_line != ""): + changed_line1_sit = 4 + lw.append(module_path + " : " + str(line_nb) + " at ) ( not at the beginning of an empty line") + + if (edit_files): + # situation 1: clear before ) ( + if (pos_paranth2 > 0 and rest_of_line == ""): + changed_line1_sit = 1 + + aux = list(list_of_lines[line_nb-1]) + auxf = [")", " ", "("] + l = 0 + for c in aux: + # remove the ), the space and the ( + if (l != pos_paranth2 and (l != pos_paranth2 + 1) and l != pos_paranth1): + auxf.append(c) + l += 1 + list_of_lines[line_nb-1] = "".join(auxf) + changed_line1 = line_nb + + # situation 2: add a newline + if (pos_paranth2 == 0 and rest_of_line != ""): + changed_line1_sit = 2 + + aux = list(list_of_lines[line_nb-1]) + auxf = [] + l = 0 + for c in aux: + # remove the ), the space and the ( + if (l != pos_paranth2 and (l != pos_paranth2 + 1) and l != pos_paranth1): + auxf.append(c) + l += 1 + list_of_lines[line_nb-1] = "".join(auxf) + changed_line1 = line_nb + + # situation 3: clear before ) ( and add a newline + if (pos_paranth2 > 0 and rest_of_line != ""): + changed_line1_sit = 3 + + aux = list(list_of_lines[line_nb-1]) + auxf = [] + l = 0 + for c in aux: + # remove the ), the space and the ( + if (l != pos_paranth2 and (l != pos_paranth2 + 1) and l != pos_paranth1): + auxf.append(c) + l += 1 + list_of_lines[line_nb-1] = "".join(auxf) + changed_line1 = line_nb + else: + lw.append(module_path + " : " + str(line_nb) + " at ) ( has to have exactly 1 space between") + + # if still inside the module declaration and regardless of params + if (name_found and end_line == -1): + pos_closing = line.find(");") + + if (pos_closing >= 0): + end_line = line_nb + if ((last_iodef_line + 1 != line_nb) or (pos_closing >= 0)): + rest_of_line = line.strip().strip(";").strip().strip(")") + + if (pos_closing > 0 or rest_of_line != ""): + lw.append(module_path + " : " + str(line_nb) + " at ); not at the beginning of the next line after the last port") + + if (edit_files): + if (pos_closing > 0 or rest_of_line != ""): + aux = list(list_of_lines[line_nb-1]) + auxf = [] + l = 0 + for c in aux: + # remove the ) and ; + if (l != pos_closing and (l != pos_closing + 1)): + auxf.append(c) + l += 1 + list_of_lines[line_nb-1] = "".join(auxf) + changed_line2 = line_nb + + # GC: check for indentation of the file + ## if it's a regular line + if ((pos_module == -1) and (pos_endmodule == -1) + and (not only_spaces_or_tabs(line)) + and (not is_comment(line)) and (not is_multiline_comment(line)) + and passed_module and (not passed_endmodule) + and (line.find("`") == -1)): + indent_nb = len(line) - len(line.lstrip()) + + if (not (indent_nb >= 2)): + if (line_nb != (last_paramdef_line+1) and line_nb != (last_iodef_line+1)): + lw.append(module_path + " : " + str(line_nb) + " no indentation found") + else: + # take only iodef from modules and not from functions also + if (indent_nb != 2 and is_paramdef(line)): + lw.append(module_path + " : " + str(line_nb) + " indentation is not proper") + + # get the module name by reading the line that contains "module" + # GC: check for proper positioning of the module declaration + if ((not is_comment(line)) and (not name_found)): + if (pos_module == 0): + ## situations accepted + ## 1. module module_name ( + ## 2. module module_name #( + + pos_diez = line.find("#") + # 2nd situation + if (pos_diez > 0): + if (pos_paranth1 == pos_diez + 1): + module_name = re.search("module(.*?)#\(", line) + if (module_name != None): + module_name = module_name.group(1) + module_name = module_name.strip() + name_found = True + else: + lw.append(module_path + " : " + str(line_nb) + " at module name - error") + else: + lw.append(module_path + " : " + str(line_nb) + " at module #( guideline not respected") + + params_exist = True + # 1st situation + else: + module_name = line.strip("module") + module_name = module_name.strip() + module_name = module_name.strip("\n") + module_name = module_name.strip() + module_name = module_name.strip("(") + module_name = module_name.strip() + name_found = True + line_nb += 1 + + if (edit_files): + if (changed_line1 != -1): + if (changed_line1_sit == 2 or changed_line1_sit == 3): + list_of_lines.insert(changed_line1, ") (\n") + + if (changed_line2 != -1): + if (changed_line1 != -1 and changed_line1_sit > 1): + changed_line2 += 1 + last_iodef_line += 1 + ## +1 -1 because we want on the next line after the last iodef line, + ## but also the counting with line_nb starts from 1, and in + ## files it starts from 0 + list_of_lines.insert((last_iodef_line + 1) - 1, ");\n") + + # GC: check for lines after endmodule and empty lines + # (and delete them, if desired) + prev_length = len(list_of_lines) + check_extra_lines (module_path, list_of_lines, lw, edit_files) + + if (edit_files): + # if at least one of the things was edited + if (changed_line1 != -1 or changed_line2 != -1 or extra_chars + or prev_length != len(list_of_lines) or (header_status == 1)): + + # then rewrite the file + with open(module_path, "w") as f: + for line in list_of_lines: + + # GC: check for whitespace at the end of the line w\o \n + aux_line = line[:-1] + aux_line = aux_line.rstrip() + + f.write(aux_line + "\n") + if (extra_chars): + lw.append(module_path + " : removed extra spaces at the end of lines") + + if (not name_found): + lw.append(module_path + " : module name couldn't be extracted\n") + + lw_last_size = len(lw) + if (lw_last_size == lw_initial_size + 1): + lw.pop() + + return module_name + + +############################################################################### +# +# Find all occurrences of the given module (path) in all files from the given +# directory (recursively, but only in \library or \projects) or in all files +# from list_of_files (if specified). +# Return list of paths (for the occurrences) relative to the given directory. +############################################################################### +def find_occurrences (directory, module_name, list_of_files): + + occurrences_list = [] + for folder, dirs, files in os.walk(directory): + + ## only folder paths without a dot + ## and to be either from /library or from /projects + if (not ((folder[1:-2]).find(".") == -1 + and (folder.find("library") != -1 or folder.find("projects") != -1))): + continue + + for file in files: + fullpath = os.path.join(folder, file) + + if (not check_filename(fullpath)): + continue + + search = False + if (list_of_files and (string_in_list(fullpath, list_of_files))): + search = True + elif (not list_of_files): + search = True + + ## the file with the module definition is not accepted and + ## neither the files that have to be avoided + if (search and file != (module_name + ".v")): + with codecs.open(fullpath, 'r', encoding='utf-8', errors='ignore') as f: + line_nb = 1 + + for line in f: + if ((line.find(module_name) != -1) and (not is_comment(line))): + pos = line.find(module_name) + pos_dot = line.find(".") + + # if there is no dot before the module name + if (pos_dot == -1 or pos < pos_dot): + if ((line[pos+len(module_name)] == ' ') or (line[pos+len(module_name)] == '#') + or (line[pos+len(module_name)] == '(') or (line[pos+len(module_name)] == '\t')): + # if before the instance name there are only spaces, then it is ok + if (only_spaces_or_tabs(line[:pos-1]) == True): + new_occurrence = Occurrence(path=fullpath, line=line_nb) + ## check if it has a parameters list; + ## then instance name is on the same line + if ("#" not in line): + new_occurrence.pos_start_ports = 0 + occurrences_list.append(new_occurrence) + line_nb += 1 + return occurrences_list + + +############################################################################### +# +# Find the lines where an occurrence starts, ends and where its list of ports +# starts. +# Return nothing (the occurrence_item fields are directly modified) +############################################################################### +def set_occurrence_lines (occurrence_item, list_of_lines): + + pos_start_module = -1 + pos_end_module = -1 + param_exist = False + instance_lines = [] + + line_nb = 1 + # find the start and the end line of the module instance + for line in list_of_lines: + if (pos_end_module == -1): + if (occurrence_item.line == line_nb): + pos_start_module = line_nb + + if ("#" in line): + param_exist = True + + # if we are inside of the module instance + if (pos_start_module != -1): + if (line.find(");") != -1): + pos_end_module = line_nb + occurrence_item.line_end = pos_end_module + else: + break + line_nb += 1 + + if (not param_exist): + occurrence_item.pos_start_ports = 0 + else: + # with parameters: get the ports' list in all_inst_lines, including parameters + all_inst_lines = "" + line_nb = 1 + for line in list_of_lines: + if (pos_start_module <= line_nb and line_nb <= pos_end_module): + all_inst_lines = all_inst_lines + line + elif (line_nb > pos_end_module): + break + line_nb += 1 + + ## find the line where the instance name is; + ## the ports should start from the next line, which is pos_start_ports+1 + + # find a string that is spread over multiple lines + aux_instance_name = re.findall('\)\n(.*?)\(', all_inst_lines, re.M) + + # if )\n i_... ( + if (len(aux_instance_name) > 0): + instance_name = aux_instance_name[0].strip(" ") + else: + # if ) i_... ( + instance_name = re.findall('\)(.*?)\(', all_inst_lines, re.M)[0].strip(" ") + + line_nb = 1 + pos_start_ports = -1 + # update occurrence_item.pos_start_ports if it wasn't already set + for line in list_of_lines: + if (pos_start_module <= line_nb and line_nb <= pos_end_module): + if ((instance_name in line) and (pos_start_ports == -1)): + # if not already specified in find_occurrences, without a parameters list + if (occurrence_item.pos_start_ports == -1): + pos_start_ports = line_nb - pos_start_module + occurrence_item.pos_start_ports = pos_start_ports + elif (line_nb > pos_end_module): + break + line_nb += 1 + + +############################################################################### +# +# Check for the guideline rules applied to the module instaces and output +# warnings for each line, if any. +############################################################################### +def check_guideline_instances (occurrence_item, lw): + + # list of warnings + lw_initial_size = len(lw) + lw.append("\nAt instances:") + + with open(occurrence_item.path, 'r') as in_file: + list_of_lines = in_file.readlines() + + # have all the fields of the occurrence_item + set_occurrence_lines(occurrence_item, list_of_lines) + + ## with parameters: get the module instance's lines in all_inst_lines, + ## including the parameters + all_inst_lines = "" + line_nb = 1 + + for line in list_of_lines: + if (occurrence_item.line <= line_nb and line_nb <= occurrence_item.line_end): + all_inst_lines = all_inst_lines + line + elif (line_nb > occurrence_item.line_end): + break + line_nb += 1 + + port_pos = 0 + line_nb = 1 + spaces_nb = -1 + passed_module = False + passed_endmodule = False + + for line in list_of_lines: + inside_module_instance = False + line_start_ports = occurrence_item.line + occurrence_item.pos_start_ports + + if ((occurrence_item.line <= line_nb) and (line_nb <= occurrence_item.line_end)): + inside_module_instance = True + + # GC: indentation for the line where the instance name is + if (line_start_ports == line_nb): + spaces_nb = len(line) - len(line.lstrip()) + if ((spaces_nb <= 0) or (spaces_nb % 2 != 0)): + lw.append(occurrence_item.path + " : " + str(line_nb) + " wrong indentation at instance name") + + # GC: indentation for the line where the module name is + if (occurrence_item.line == line_nb): + start_spaces_nb = len(line) - len(line.lstrip()) + if ((start_spaces_nb <= 0) or (start_spaces_nb % 2 != 0)): + lw.append(occurrence_item.path + " : " + str(line_nb) + " wrong indentation at module name") + + # GC: check for proper positioning of the module instance + if (inside_module_instance): + if ("#" in line): + diez_ok = False + + if ("." in line): + lw.append(occurrence_item.path + " : " + str(line_nb) + " #(. in module instance") + else: + pos_diez = line.find("#") + pos_paranth1 = line.find("(") + pos_paranth2 = line.find(")") + + if ((0 < pos_diez) and (pos_diez + 1 == pos_paranth1) and (pos_paranth2 == -1)): + diez_ok = True + else: + if (pos_paranth2 != -1): + lw.append(occurrence_item.path + " : " + str(line_nb) + " parameters must be each on its own line") + else: + lw.append(occurrence_item.path + " : " + str(line_nb) + " parameters list is not written ok") + + # for the line where the instance name is + # find a string like )\n ... ( + aux_instance_name = re.findall('\)\n(.*?)\(', all_inst_lines, re.M) + instance_name = "" + + # if )\n i_... ( + if (len(aux_instance_name) > 0): + instance_name = aux_instance_name[0].strip(" ") + if (")" not in instance_name): + lw.append(occurrence_item.path + " : " + str(line_start_ports) + " ) i_... ( instance name not written ok") + + else: + try: + # if ) i_... ( + instance_name = re.findall('\)(.*?)\(', all_inst_lines, re.M)[0].strip(" ") + except Exception: + lw.append(occurrence_item.path + " : " + str(occurrence_item.line + occurrence_item.pos_start_ports) + " couldn't extract instance name") + + pos_dot = line.find(".") + pos_comma = line.find(",") + pos_closing = line.find(");") + + # GC: all ); of instances cannot be on an empty line + aux_line = line.strip() + aux_line = aux_line.strip("\t") + aux_line = aux_line.strip(")") + aux_line = aux_line.strip(";") + if ((pos_closing != -1) and (only_spaces_or_tabs(aux_line))): + lw.append(occurrence_item.path + " : " + str(line_nb) + " ); when closing module instance") + + # every dot starting from (.line + .pos_start_ports) line means a new port is declared + if ((line_start_ports <= line_nb) and (pos_dot != -1)): + port_indentation = len(line) - len(line.lstrip()) + port_pos += 1 + + # 1. the first port in the module instance + # 2. anywhere inside the instance, but not the first or last + # 3. when .port()); + # 4. last port when .port()\n and ); is on the next line + + # no situation or error situation + situation = 0 + inst_closed = False + if (pos_closing != -1): + inst_closed = True + + # 1st situation + if (port_pos == 1): + situation = 1 + else: + # 3rd situation + if ((pos_dot != -1) and inst_closed): + situation = 3 + else: + # 4th situation + if ((pos_dot != -1) and (pos_comma == -1) and (not inst_closed)): + situation = 4 + else: + # 2nd situation + if ((pos_dot != -1) and (pos_comma != -1) and (not inst_closed)): + situation = 2 + else: + lw.append(occurrence_item.path + " : " + str(line_nb) + " problem when finding the situation") + + if (situation != 0): + # the rest of the ports must have the same indentation as the previous line + if (port_indentation - spaces_nb != 2): + avoid_indentation_check = False + + if ((line.find("({") != -1) or (line.find("})") != -1)): + avoid_indentation_check = True + + if (not avoid_indentation_check): + lw.append(occurrence_item.path + " : " + str(line_nb) + " indentation inside module instance") + else: + # if inside the parameters list + if (occurrence_item.line <= line_nb and line_nb < line_start_ports and (pos_dot != -1)): + param_indentation = len(line) - len(line.lstrip()) + if (param_indentation - start_spaces_nb != 2): + lw.append(occurrence_item.path + " : " + str(line_nb) + " indentation inside parameters list") + + line_nb += 1 + + if (line_nb > occurrence_item.line_end): + break + + lw_last_size = len(lw) + + if (lw_last_size == lw_initial_size + 1): + lw.pop() + +############################################################################### +# +# Check guideline for Verilog files in repository +############################################################################### + +## all files given as parameters to the script (or all files from repo +## if no flag is specified) +modified_files = [] +error_files = [] +edit_files = False +guideline_ok = True +# detect all modules from current directory (hdl) +all_modules = detect_all_modules("./") + +xilinx_modules = [] +xilinx_modules.append("ALT_IOBUF") +xilinx_modules.append("BUFG") +xilinx_modules.append("BUFG_GT") +xilinx_modules.append("BUFGCE") +#xilinx_modules.append("BUFGCE_1") +xilinx_modules.append("BUFGCE_DIV") +xilinx_modules.append("BUFGCTRL") +xilinx_modules.append("BUFGMUX") +#xilinx_modules.append("BUFGMUX_1") +xilinx_modules.append("BUFGMUX_CTRL") +xilinx_modules.append("BUFIO") +xilinx_modules.append("BUFR") +xilinx_modules.append("GTHE3_CHANNEL") +xilinx_modules.append("GTHE4_CHANNEL") +xilinx_modules.append("GTYE4_CHANNEL") +xilinx_modules.append("GTXE2_CHANNEL") +xilinx_modules.append("IBUFDS") +xilinx_modules.append("IBUFDS_GTE2") +xilinx_modules.append("IBUFDS_GTE3") +xilinx_modules.append("IBUFDS_GTE4") +xilinx_modules.append("IBUFDS_GTE5") +xilinx_modules.append("IBUFG") +xilinx_modules.append("IDDR") +xilinx_modules.append("IDELAYCTRL") +xilinx_modules.append("ISERDESE2") +xilinx_modules.append("OBUFDS") +xilinx_modules.append("ODDR") +xilinx_modules.append("system_bd") +xilinx_modules.append("system_wrapper") + +# if there is an argument specified +if (len(sys.argv) > 1): + + # -m means a file name/s will be specified (including extension!) + # mostly used for testing manually, changing the folder_path + # -me means that it will also modify the files + if (sys.argv[1] == "-m" or sys.argv[1] == "-me"): + if (sys.argv[1] == "-me"): + edit_files = True + + arg_nb = 2 + + while (arg_nb < len(sys.argv)): + # look in the folder_path = current folder + for folder, dirs, files in os.walk("./"): + for name in files: + if((name == sys.argv[arg_nb]) and (check_filename(name))): + #module_path = os.path.abspath(os.path.join(folder, sys.argv[arg_nb])) + module_path = os.path.join(folder, sys.argv[arg_nb]) + modified_files.append(module_path) + arg_nb += 1 + + # -p means a path/s will be specified + # mostly used for github action + # -pe means that it will also modify the files + if (sys.argv[1] == "-p" or sys.argv[1] == "-pe"): + if (sys.argv[1] == "-pe"): + edit_files = True + + arg_nb = 2 + while (arg_nb < len(sys.argv)): + if (os.path.exists(sys.argv[arg_nb])): + if (check_filename(sys.argv[arg_nb])): + modified_files.append(sys.argv[arg_nb]) + else: + error_files.append(sys.argv[arg_nb]) + arg_nb += 1 + + # -e means it will be run on all files, making changes in them + if (sys.argv[1] == "-e"): + edit_files = True + modified_files = detect_all_modules("./") + +else: + ## if there is no argument then the script is run on all files, + ## and without making changes in them + edit_files = False + modified_files = detect_all_modules("./") + +# no matter the number of arguments +if (len(modified_files) <= 0): + print("NO detected modules") + guideline_ok = True + sys.exit(0) +else: + for module_path in all_modules: + module_name = get_file_name(module_path) + # list of warnings + lw = [] + + # if the detected module is between the modified files + if (string_in_list(module_path, modified_files)): + module_name = get_and_check_module(module_path, lw, edit_files) + file_name = get_file_name(module_path) + + # file_name is without the known extension, which is .v + if (module_name != file_name): + # applies only to the library folder + if (module_path.find("library") != -1): + guideline_ok = False + error_files.append(module_path) + + ## system_top modules won't be instantiated anywhere in other + ## Verilog or SystemVerilog files + if (module_path.find("system_top") == -1): + # will search for instances only in the files given as arguments + occurrences_list = find_occurrences("./", module_name, modified_files) + if (len(occurrences_list) > 0): + for occurrence_item in occurrences_list: + check_guideline_instances(occurrence_item, lw) + + if (len(lw) > 0): + guideline_ok = False + print ("\n -> For %s in:" % module_path) + for message in lw: + print(message) + + for module_name in xilinx_modules: + lw = [] + xilinx_occ_list = find_occurrences("./", module_name, modified_files) + + if (len(xilinx_occ_list) > 0): + for xilinx_occ_it in xilinx_occ_list: + # if the xilinx module was found in the files that are of interest + for it in all_modules: + if (xilinx_occ_it.path == it): + # only then to check the guideline + check_guideline_instances(xilinx_occ_it, lw) + + if (len(lw) > 0): + title_printed = False + + for message in lw: + if (list_has_substring(modified_files, message)): + if (not title_printed): + print ("\n -> For %s in:" % module_name) + title_printed = True + guideline_ok = False + print(message) + + if (error_files): + error_in_library = False + + for file in error_files: + ## for files in /projects folder, + ## the module - file name check doesn't matter + if (file.find("library") != -1): + error_in_library = True + + if (error_in_library): + guideline_ok = False + print ("Files with name errors:") + for file in error_files: + ## for files in /projects folder, + ## the module - file name check doesn't matter + if (file.find("library") != -1): + print (file) + + if (not guideline_ok): + print("\nGuideline not respected\n") + sys.exit(1) + else: + sys.exit(0) diff --git a/.github/scripts/readme_check_guideline.md b/.github/scripts/readme_check_guideline.md new file mode 100644 index 000000000..c697eb4bd --- /dev/null +++ b/.github/scripts/readme_check_guideline.md @@ -0,0 +1,107 @@ +# User guide for [check_guideline.py](https://github.com/analogdevicesinc/hdl/tree/master/.github/scripts/check_guideline.py) + +## Prerequisites + +* the script must be run while being in the root directory (/hdl) +* clean the repository to remove the files generated by Vivado +* it will be run only on Verilog and SystemVerilog files that do not contain "tb" in their path +* uses Python 3.x + +## Rules that are checked + +These rules can be found in the [HDL coding guideline](https://github.com/analogdevicesinc/hdl/blob/master/docs/hdl_coding_guideline.md). + +### 1. License header + +It checks if the license header is up-to-date, containing the current year in +the year range. Exceptions are the JESD files and the ones specified in the +`avoid_list` string list. +If `-e` option is added, the script can update the year range. + +### 2. Two or more consecutive empty lines + +It checks in the whole file if there are two or more consecutive empty lines. +If `-e` option is added, the script can remove them and leave only one empty line. + +### 3. Trailing whitespace + +It checks if there are whitespace characters at the end of the lines. +If `-e` option is added, then they can be removed. + +### 4. Lines after `endmodule` tag + +It checks if there are lines after it. +If `-e` option is added, the script can remove them. + +### 5. Parentheses around the module declaration + +It checks if the parentheses around the module declaration (meaning `) (` for +the parameters' list) are on an empty line, right after the last parameter. +It also checks for the closing parenthesis at the module declaration (meaning `);`) +to be on an empty line, at the beginning, right after the last I/O port line. +If `-e` option is added, the script can put them in their proper place. + +### 6. Indentation of code + +It checks if all lines (except for the ones that are commented) have an indentation +of two or multiple of two spaces. +Other exceptions to this are the `module` line, the `endmodule` line, the `) (` +and the `);` from the module declaration. + +### 7. Position of the module instances + +It checks if the parameters' list (if that's the case) is in proper position, +meaning that the position of `#` is checked, the parameters to be specified each +on its own line, the parentheses around the instance name and the closing parenthesis +of the module instance. + +_NOTE_: these rules are marked in the script with **GC** (stands for Guideline Check) +in the comments. + +## Changes done by the script to your files + +If one wants the script to make changes in files, they will be regarding: +* license header, except for JESD files and the ones specified in `avoid_list` +* two or more consecutive empty lines +* trailing whitespaces +* lines after `endmodule` tag +* parentheses around the module declaration (meaning `) (` for the parameters' + list and `);` for when closing the declaration) + +## Ways to run the script + +1. With no arguments: `python3 check_guideline.py` +Checks all files with the properties specified above. +Does not modify the files. + +2. With arguments: + 1. `-e` with no file specified + Checks all files with the properties specified above. Additionally, + it modifies the module definition parentheses according to the guideline. + + 2. `-m` + Checks files that are given as arguments (by their names including the + extension). + + 3. `-me` + Checks files that are given as arguments (by their names including the + extension) and modifies the files where the guideline is not respected. + + 4. `-p` + Checks files that are given as arguments (by their relative path) and + modifies the files where the guideline is not respected. + + 5. `-pe` + Checks files that are given as arguments (by their relative path) and + modifies the files where the guideline is not respected. + +## Examples of running + +``` +python3 check_guideline.py >> warnings.txt +python3 check_guideline.py -e >> warnings.txt +python3 check_guideline.py -m axi_ad9783.v >> warnings.txt +python3 check_guideline.py -me axi_ad9783.v axi_ad9783_if.v up_adc_common.v >> warnings.txt +python3 check_guideline.py -p ./library/axi_ad9783/axi_ad9783.v ./library/common/up_adc_common.v >> warnings.txt +python3 check_guideline.py -pe ./library/axi_ad9783/axi_ad9783_if.v >> warnings.txt +```