Coverage for src/castep_linter/scan_files.py: 36%

57 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-23 18:07 +0000

1"""Static code analysis tool for castep""" 

2import argparse 

3import logging 

4import pathlib 

5import sys 

6 

7from rich.console import Console 

8from tree_sitter import Parser 

9 

10from castep_linter import error_logging 

11from castep_linter.error_logging.xml_writer import write_xml 

12from castep_linter.fortran import parser 

13from castep_linter.tests import test_list 

14 

15# done - complex(var) vs complex(var,dp) or complex(var, kind=dp) 

16# done - allocate without stat and stat not checked. deallocate? 

17# done - integer_dp etc 

18# real with trailing . not .0 or .0_dp? 

19# io_allocate_abort with wrong subname 

20# tabs & DOS line endings, whitespace, comments? 

21 

22 

23def run_tests_on_code( 

24 fort_parser: Parser, code: bytes, test_dict: dict, filename: str 

25) -> error_logging.ErrorLogger: 

26 """Run all available tests on the supplied source code""" 

27 tree = fort_parser.parse(code) 

28 error_log = error_logging.ErrorLogger(filename) 

29 

30 for node in parser.traverse_tree(tree): 

31 # Have to check for is_named here as we want the statements, 

32 # not literal words like subroutine 

33 if node.type in test_dict: 

34 for test in test_dict[node.type]: 

35 test(node, error_log) 

36 

37 return error_log 

38 

39 

40def path(arg: str) -> pathlib.Path: 

41 """Check a file exists and if so, return a path object""" 

42 my_file = pathlib.Path(arg) 

43 if not my_file.is_file(): 

44 err = f"The file {arg} does not exist!" 

45 raise argparse.ArgumentTypeError(err) 

46 return my_file 

47 

48 

49def parse_args(): 

50 """Parse the command line args for a message print level and a list of filenames""" 

51 arg_parser = argparse.ArgumentParser(prog="castep-linter", description="Code linter for CASTEP") 

52 arg_parser.add_argument( 

53 "-l", 

54 "--level", 

55 help="Error message level", 

56 default="Info", 

57 choices=error_logging.ERROR_SEVERITY.keys(), 

58 ) 

59 arg_parser.add_argument( 

60 "-x", "--xml", type=pathlib.Path, help="File for JUnit xml output if required" 

61 ) 

62 arg_parser.add_argument("-q", "--quiet", action="store_true", help="Do not write to console") 

63 arg_parser.add_argument("-d", "--debug", action="store_true", help="Turn on debug output") 

64 arg_parser.add_argument("file", nargs="+", type=path, help="Files to scan") 

65 return arg_parser.parse_args() 

66 

67 

68def main() -> None: 

69 """Main entry point for the CASTEP linter""" 

70 args = parse_args() 

71 

72 fortran_parser = parser.get_fortran_parser() 

73 console = Console(soft_wrap=True) 

74 

75 if args.debug: 

76 logging.basicConfig(level=logging.DEBUG) 

77 

78 error_logs = {} 

79 

80 for file in args.file: 

81 with file.open("rb") as fd: 

82 raw_text = fd.read() 

83 

84 try: 

85 error_log = run_tests_on_code(fortran_parser, raw_text, test_list, str(file)) 

86 except UnicodeDecodeError: 

87 logging.error("Failed to properly decode %s", file) 

88 raise 

89 

90 if not args.quiet: 

91 error_log.print_errors(console, level=args.level) 

92 

93 err_count = error_log.count_errors() 

94 

95 console.print( 

96 f"{len(error_log.errors)} issues in {file} ({err_count['Error']} errors," 

97 f" {err_count['Warn']} warnings, {err_count['Info']} info)" 

98 ) 

99 

100 error_logs[str(file)] = error_log 

101 

102 if args.xml: 

103 write_xml(args.xml, error_logs, error_logging.ERROR_SEVERITY[args.level]) 

104 

105 if any(e.has_errors for e in error_logs.values()): 

106 sys.exit(1) 

107 else: 

108 sys.exit(0)