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
« 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
7from rich.console import Console
8from tree_sitter import Parser
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
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?
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)
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)
37 return error_log
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
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()
68def main() -> None:
69 """Main entry point for the CASTEP linter"""
70 args = parse_args()
72 fortran_parser = parser.get_fortran_parser()
73 console = Console(soft_wrap=True)
75 if args.debug:
76 logging.basicConfig(level=logging.DEBUG)
78 error_logs = {}
80 for file in args.file:
81 with file.open("rb") as fd:
82 raw_text = fd.read()
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
90 if not args.quiet:
91 error_log.print_errors(console, level=args.level)
93 err_count = error_log.count_errors()
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 )
100 error_logs[str(file)] = error_log
102 if args.xml:
103 write_xml(args.xml, error_logs, error_logging.ERROR_SEVERITY[args.level])
105 if any(e.has_errors for e in error_logs.values()):
106 sys.exit(1)
107 else:
108 sys.exit(0)