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

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

2 import argparse

3 import logging

4 import pathlib

5 import sys

6

7 from rich.console import Console

8 from tree_sitter import Parser

9

10 from castep_linter import error_logging

11 from castep_linter.error_logging.xml_writer import write_xml

12 from castep_linter.fortran import parser

13 from 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

23 def 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

40 def 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

49 def 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

68 def 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)