diff --git a/tools/chapel-py/examples/replace.py b/tools/chapel-py/examples/replace.py index 54b4a6ce3730..b15198865d14 100644 --- a/tools/chapel-py/examples/replace.py +++ b/tools/chapel-py/examples/replace.py @@ -26,51 +26,67 @@ # Application-Specific Code # ------------------------- + def find_this_complete(rc, root): # pattern for x._() - methodcall = [FnCall, ["?dot", Dot, ["?ident", Identifier]], chapel.rest] + methodcall = [FnCall, ["?dot", Dot, ["?ident", Identifier]], chapel.rest] + + for node, variables in chapel.each_matching(root, methodcall): + if ( + variables["dot"].field() == "complete" + and variables["ident"].name() == "this" + ): + yield (node, "init this") - for (node, variables) in chapel.each_matching(root, methodcall): - if variables["dot"].field() == "complete" and variables["ident"].name() == "this": - yield (node, 'init this') def rename_x_y_to_a_b(rc, root): - for (fn, _) in chapel.each_matching(root, Function): - if fn.name() != "changeMe": continue + for fn, _ in chapel.each_matching(root, Function): + if fn.name() != "changeMe": + continue - yield from rename_formals(rc, fn, { "x": "a", "y": "b" }) + yield from rename_formals(rc, fn, {"x": "a", "y": "b"}) # pattern for x(...) fncall = [FnCall, ["?ident", Identifier], chapel.rest] - for (call, variables) in chapel.each_matching(root, fncall): - if variables["ident"].name() != "changeMe": continue + for call, variables in chapel.each_matching(root, fncall): + if variables["ident"].name() != "changeMe": + continue + + yield from rename_named_actuals(rc, call, {"x": "a", "y": "b"}) - yield from rename_named_actuals(rc, call, { "x": "a", "y": "b" }) def tag_all_nodes_assert_on_gpu(rc, root): # pattern for x() noargcall = [FnCall, ["?ident", Identifier]] - for (foreach, _) in chapel.each_matching(root, Foreach): + for foreach, _ in chapel.each_matching(root, Foreach): has_assert_on_gpu = False - loop_body = list(foreach)[-1]; + loop_body = list(foreach)[-1] for child in loop_body: variables = chapel.match_pattern(child, noargcall) - if variables is not None and variables["ident"].name() == "assertOnGpu": + if ( + variables is not None + and variables["ident"].name() == "assertOnGpu" + ): has_assert_on_gpu = True - yield (child, '') + yield (child, "") indent = rc.node_indent(foreach) if has_assert_on_gpu: - yield (foreach, lambda text, i = indent: "@assertOnGpu\n" + (" " * i) + text) + yield ( + foreach, + lambda text, i=indent: "@assertOnGpu\n" + (" " * i) + text, + ) + def tag_aggregates_with_io_interfaces(rc, root): aggrs_to_change = defaultdict(lambda: set()) names_to_tag = defaultdict(lambda: set()) - for (fn, _) in chapel.each_matching(root, Function): - if not fn.is_method(): continue + for fn, _ in chapel.each_matching(root, Function): + if not fn.is_method(): + continue name = fn.name() if name == "serialize": @@ -80,11 +96,17 @@ def tag_aggregates_with_io_interfaces(rc, root): elif name == "init": formal_names = [] for child in fn: - if not isinstance(child, Formal): continue - if child.name() == "this": continue + if not isinstance(child, Formal): + continue + if child.name() == "this": + continue formal_names.append(child.name()) - if len(formal_names) >=2 and formal_names[-1] == "deserializer" and formal_names[-2] == "reader": + if ( + len(formal_names) >= 2 + and formal_names[-1] == "deserializer" + and formal_names[-2] == "reader" + ): tag = "initDeserializable" else: continue @@ -99,20 +121,26 @@ def tag_aggregates_with_io_interfaces(rc, root): names_to_tag[rc.node_exact_string(this_receiver)].add(tag) def build_tag_str(tags): - if len(tags) == 3: return "serializable" + if len(tags) == 3: + return "serializable" # tags have a preferred order, so just use an if-else chain to make that work - the_order = ["writeSerializable", "readDeserializable", "initDeserializable"] + the_order = [ + "writeSerializable", + "readDeserializable", + "initDeserializable", + ] return ", ".join(t for t in the_order if t in tags) - for (record, _) in chapel.each_matching(root, AggregateDecl): + for record, _ in chapel.each_matching(root, AggregateDecl): tags = set() if record.unique_id() in aggrs_to_change: tags |= aggrs_to_change[record.unique_id()] if record.name() in names_to_tag: tags |= names_to_tag[record.name()] - if len(tags) == 0: continue + if len(tags) == 0: + continue tag_str = build_tag_str(tags) record_text = rc.node_exact_string(record) @@ -120,10 +148,13 @@ def build_tag_str(tags): colonpos = record_text.find(":") if colonpos >= 0 and colonpos < curlypos: - new_text = record_text.replace(" {" , ", " + tag_str + " {" , 1) + new_text = record_text.replace(" {", ", " + tag_str + " {", 1) else: - new_text = record_text.replace(record.name(), record.name() + " : " + tag_str, 1) + new_text = record_text.replace( + record.name(), record.name() + " : " + tag_str, 1 + ) yield (record, new_text) + run(fuse(find_this_complete, rename_x_y_to_a_b, tag_all_nodes_assert_on_gpu)) diff --git a/tools/chapel-py/setup.py b/tools/chapel-py/setup.py index 5e63ed3e5f8c..42e200c5c1fa 100644 --- a/tools/chapel-py/setup.py +++ b/tools/chapel-py/setup.py @@ -24,9 +24,16 @@ import sys import glob -chpl_home = str(os.getenv('CHPL_HOME')) +chpl_home = str(os.getenv("CHPL_HOME")) chpl_printchplenv = os.path.join(chpl_home, "util", "printchplenv") -chpl_variables_lines = subprocess.check_output([chpl_printchplenv, "--internal", "--all", " --anonymize", "--simple"]).decode(sys.stdout.encoding).strip().splitlines() +chpl_variables_lines = ( + subprocess.check_output( + [chpl_printchplenv, "--internal", "--all", " --anonymize", "--simple"] + ) + .decode(sys.stdout.encoding) + .strip() + .splitlines() +) chpl_variables = dict() for line in chpl_variables_lines: elms = line.split("=", maxsplit=1) @@ -44,11 +51,21 @@ CXXFLAGS += ["-DHAVE_LLVM"] CXXFLAGS += ["-Wno-c99-designator"] -CXXFLAGS += subprocess.check_output([llvm_config, "--cxxflags"]).decode(sys.stdout.encoding).strip().split() +CXXFLAGS += ( + subprocess.check_output([llvm_config, "--cxxflags"]) + .decode(sys.stdout.encoding) + .strip() + .split() +) CXXFLAGS += ["-std=c++17", "-I{}/frontend/include".format(chpl_home)] LDFLAGS = [] -LDFLAGS += ["-L{}".format(chpl_lib_path), "-lChplFrontendShared", "-Wl,-rpath", chpl_lib_path] +LDFLAGS += [ + "-L{}".format(chpl_lib_path), + "-lChplFrontendShared", + "-Wl,-rpath", + chpl_lib_path, +] setup( name="chapel", @@ -59,7 +76,7 @@ Extension( "chapel.core", glob.glob("src/*.cpp"), - depends = glob.glob("src/**/*.h", recursive=True), + depends=glob.glob("src/**/*.h", recursive=True), extra_compile_args=CXXFLAGS, extra_link_args=LDFLAGS, ) diff --git a/tools/chapel-py/src/chapel/__init__.py b/tools/chapel-py/src/chapel/__init__.py index 3235812d271a..0ce19ffec5ce 100644 --- a/tools/chapel-py/src/chapel/__init__.py +++ b/tools/chapel-py/src/chapel/__init__.py @@ -27,6 +27,7 @@ QualifiedType = typing.Tuple[str, Optional[ChapelType], Optional[Param]] + def preorder(node): """ Recursively visit the given AST node, going in pre-order (parent-then-children) @@ -356,6 +357,7 @@ def files_with_contexts(files): for filename in to_yield: yield (filename, ctx) + def range_to_tokens( rng: Location, lines: List[str] ) -> List[typing.Tuple[int, int, int]]: @@ -396,12 +398,14 @@ def range_to_lines(rng: Location, lines: List[str]) -> List[str]: text.append(lines[line][column : column + length]) return text + def range_to_text(rng: Location, lines: List[str]) -> str: """ Convert a Chapel location to a single string """ return "\n".join(range_to_lines(rng, lines)) + def get_file_lines(context: Context, node: AstNode) -> typing.List[str]: """ Get the lines of the file containing the given node diff --git a/tools/chapel-py/src/chapel/lsp/__init__.py b/tools/chapel-py/src/chapel/lsp/__init__.py index 7baf1e08c240..2f4f9f1453ff 100644 --- a/tools/chapel-py/src/chapel/lsp/__init__.py +++ b/tools/chapel-py/src/chapel/lsp/__init__.py @@ -33,6 +33,7 @@ def location_to_range(location) -> Range: end=Position(max(0, end[0] - 1), max(end[1] - 1, 0)), ) + def error_to_diagnostic(error) -> Diagnostic: """ Convert a Chapel error into a lsprotocol.types Diagnostic diff --git a/tools/chapel-py/src/chapel/replace/__init__.py b/tools/chapel-py/src/chapel/replace/__init__.py index a13f901ae3c6..01ff5b77bf49 100644 --- a/tools/chapel-py/src/chapel/replace/__init__.py +++ b/tools/chapel-py/src/chapel/replace/__init__.py @@ -24,6 +24,7 @@ import sys import typing + class ReplacementContext: """ This class is given as an argument to 'finder' functions so that @@ -34,7 +35,6 @@ class ReplacementContext: contains that information. """ - def __init__(self, path: str): """ Given a file path, creates a ReplacementContext that contains the @@ -47,11 +47,13 @@ def __init__(self, path: str): self.lines = {1: 0} self.lines_back = {} line = 1 - for (i, char) in enumerate(self.content): + for i, char in enumerate(self.content): self.lines_back[i] = line - if char == '\n': + if char == "\n": line += 1 - self.lines[line] = i+1 # the next characrer is the start of the next line + self.lines[line] = ( + i + 1 + ) # the next characrer is the start of the next line def loc_to_idx(self, loc: (int, int)) -> int: """ @@ -88,6 +90,7 @@ def node_indent(self, node: chapel.AstNode) -> int: (range_start, _) = self.node_idx_range(node) return range_start - self.lines[self.lines_back[range_start]] + def rename_formals(rc: ReplacementContext, fn: chapel.Function, renames): """ Helper iterator to be used in finder functions. Given a function @@ -100,10 +103,12 @@ def name_replacer(name): for child in fn.formals(): name = child.name() - if name not in renames: continue + if name not in renames: + continue yield (child, name_replacer(name)) + def rename_named_actuals(rc: ReplacementContext, call: chapel.Call, renames): """ Helper iterator to be used in finder functions. Given a function call expression, @@ -113,7 +118,8 @@ def rename_named_actuals(rc: ReplacementContext, call: chapel.Call, renames): for actual in call.actuals(): if isinstance(actual, tuple): (name, actual) = actual - if name not in renames: continue + if name not in renames: + continue actual_text = rc.node_exact_string(actual) @@ -122,9 +128,9 @@ def rename_named_actuals(rc: ReplacementContext, call: chapel.Call, renames): yield from [] -def replace(finder: typing.Generator, - ctx: chapel.Context, - filename: str) -> str: +def replace( + finder: typing.Generator, ctx: chapel.Context, filename: str +) -> str: """ Drives replacement of text based on matches found in `finder`. """ @@ -142,7 +148,7 @@ def compose(outer, inner): return lambda text: outer(inner(text)) for ast in asts: - for (node, replace_with) in finder(rc, ast): + for node, replace_with in finder(rc, ast): uid = node.unique_id() # Old result doesn't matter or doesn't exist; throw it out. if not callable(replace_with) or uid not in nodes_to_replace: @@ -153,7 +159,9 @@ def compose(outer, inner): elif uid in nodes_to_replace: # Old substitution is also a callable; need to create composition. if callable(nodes_to_replace[uid]): - nodes_to_replace[uid] = compose(replace_with, nodes_to_replace[uid]) + nodes_to_replace[uid] = compose( + replace_with, nodes_to_replace[uid] + ) # Old substitution is a string; we can apply the callable to get # another string. else: @@ -181,7 +189,7 @@ def recurse(node: chapel.AstNode): (replace_from, replace_to) = rc.node_idx_range(node) my_text = rc.node_exact_string(node) for child in reversed(list(node)): - for (child_from, child_to, child_str) in recurse(child): + for child_from, child_to, child_str in recurse(child): # Child is not inside this node, so it can be replaced as before if child_from >= replace_to: yield (child_from, child_to, child_str) @@ -192,16 +200,31 @@ def recurse(node: chapel.AstNode): child_from -= replace_from child_to -= replace_from - my_text = my_text[:child_from] + child_str + my_text[child_to:] + my_text = ( + my_text[:child_from] + + child_str + + my_text[child_to:] + ) yield (replace_from, replace_to, my_replace(my_text)) for ast in reversed(asts): - for (replace_from, replace_to, replace_with) in recurse(ast): - new_content = new_content[:replace_from] + replace_with + new_content[replace_to:] + for replace_from, replace_to, replace_with in recurse(ast): + new_content = ( + new_content[:replace_from] + + replace_with + + new_content[replace_to:] + ) return new_content -def _do_replace(finder: typing.Generator, ctx: chapel.Context, filename: str, suffix: str, inplace: bool): + +def _do_replace( + finder: typing.Generator, + ctx: chapel.Context, + filename: str, + suffix: str, + inplace: bool, +): new_content = replace(finder, ctx, filename) @@ -212,7 +235,12 @@ def _do_replace(finder: typing.Generator, ctx: chapel.Context, filename: str, su with open(store_into, "w") as newfile: newfile.write(new_content) -def run(finder: typing.Generator, name:str='replace', description:str='A tool to search-and-replace Chapel expressions with others'): + +def run( + finder: typing.Generator, + name: str = "replace", + description: str = "A tool to search-and-replace Chapel expressions with others", +): """ Start a command-line replacer program with the given 'finder' function. This program will automatically support accepting a list of files on @@ -226,19 +254,26 @@ def run(finder: typing.Generator, name:str='replace', description:str='A tool to """ parser = argparse.ArgumentParser(prog=name, description=description) - parser.add_argument('filenames', nargs='*') - parser.add_argument('--suffix', dest='suffix', action='store', default='.new') - parser.add_argument('--in-place', dest='inplace', action='store_true', default=False) + parser.add_argument("filenames", nargs="*") + parser.add_argument( + "--suffix", dest="suffix", action="store", default=".new" + ) + parser.add_argument( + "--in-place", dest="inplace", action="store_true", default=False + ) args = parser.parse_args() - for (filename, ctx) in chapel.files_with_contexts(args.filenames): + for filename, ctx in chapel.files_with_contexts(args.filenames): _do_replace(finder, ctx, filename, args.suffix, args.inplace) + def fuse(*args): """ Combines multiple 'finder' iterators into one. """ + def fused(rc, root): for arg in args: yield from arg(rc, root) + return fused diff --git a/tools/chapel-py/src/chapel/visitor/__init__.py b/tools/chapel-py/src/chapel/visitor/__init__.py index 6b9ee8dbf5c4..a8c88c8c39f7 100644 --- a/tools/chapel-py/src/chapel/visitor/__init__.py +++ b/tools/chapel-py/src/chapel/visitor/__init__.py @@ -23,6 +23,7 @@ from collections import defaultdict from inspect import signature + def _try_call(dispatch_table, visitor, node, method_type): """ Not every AST node in the dispatch table has an entry, unless the user @@ -56,6 +57,7 @@ def _try_call(dispatch_table, visitor, node, method_type): node_type = node_type.__base__ return None + def _visitor_visit(dispatch_table): def _do_visit(self, node): if isinstance(node, list): @@ -67,8 +69,10 @@ def _do_visit(self, node): for child in node: _do_visit(self, child) _try_call(dispatch_table, self, node, "exit") + return _do_visit + def enter(method): """ Annotates a class method as being an 'enter' function. An 'enter' @@ -84,6 +88,7 @@ def enter(method): method.__chapel_visitor_method__ = "enter" return method + def exit(method): """ Annotates a class method as being an 'exit' function. An 'exit' @@ -93,6 +98,7 @@ def exit(method): method.__chapel_visitor_method__ = "exit" return method + def visitor(clazz): """ Marks a class as being a visitor. This will add a 'visit' method to @@ -112,22 +118,26 @@ def visitor(clazz): # Detect all visitor-like methods. for name, var in vars(clazz).items(): - if not callable(var): continue - if not hasattr(var, "__chapel_visitor_method__"): continue + if not callable(var): + continue + if not hasattr(var, "__chapel_visitor_method__"): + continue sig = signature(var) - if 'self' not in sig.parameters: continue - if len(sig.parameters) != 2: continue + if "self" not in sig.parameters: + continue + if len(sig.parameters) != 2: + continue # Detect the type of the second argument. It's an OrderedDict # so we can use numbers. arg = list(sig.parameters.values())[1] # Check if the type is a subtype of AstNode - if not issubclass(arg.annotation, chapel.core.AstNode): continue + if not issubclass(arg.annotation, chapel.core.AstNode): + continue dispatch_table[arg.annotation][var.__chapel_visitor_method__] = var clazz.visit = _visitor_visit(dispatch_table) return clazz - diff --git a/tools/chpl-language-server/src/bisect_compat.py b/tools/chpl-language-server/src/bisect_compat.py index d490e41f47f9..06fbb2aae96f 100644 --- a/tools/chpl-language-server/src/bisect_compat.py +++ b/tools/chpl-language-server/src/bisect_compat.py @@ -19,8 +19,9 @@ from typing import Callable, Sequence, Optional + # reimplement bisect with a key, since the parameter wasn't added until python 3.10+ -def bisect_right(a: Sequence, x, key: Optional[Callable]=None): +def bisect_right(a: Sequence, x, key: Optional[Callable] = None): lo = 0 hi = len(a) @@ -35,7 +36,8 @@ def bisect_right(a: Sequence, x, key: Optional[Callable]=None): lo = mid + 1 return lo -def bisect_left(a: Sequence, x, key: Optional[Callable]=None): + +def bisect_left(a: Sequence, x, key: Optional[Callable] = None): lo = 0 hi = len(a) diff --git a/tools/chpl-language-server/src/chpl-language-server.py b/tools/chpl-language-server/src/chpl-language-server.py index a65d63817162..323f00581d1f 100755 --- a/tools/chpl-language-server/src/chpl-language-server.py +++ b/tools/chpl-language-server/src/chpl-language-server.py @@ -198,8 +198,7 @@ def decl_kind_to_completion_kind(kind: SymbolKind) -> CompletionItemKind: def completion_item_for_decl( - decl: chapel.NamedDecl, - override_name: Optional[str] = None + decl: chapel.NamedDecl, override_name: Optional[str] = None ) -> Optional[CompletionItem]: kind = decl_kind(decl) if not kind: @@ -1510,7 +1509,7 @@ async def complete(ls: ChapelLanguageServer, params: CompletionParams): fi, _ = ls.get_file_info(text_doc.uri) items = [] - for (name, decl) in fi.visible_decls: + for name, decl in fi.visible_decls: if isinstance(decl, chapel.NamedDecl): items.append(completion_item_for_decl(decl, override_name=name)) items.extend(completion_item_for_decl(mod) for mod in fi.used_modules) diff --git a/tools/chpl-language-server/src/chpl-shim.py b/tools/chpl-language-server/src/chpl-shim.py index e3511285dcb1..bb1d8b5e4fd8 100755 --- a/tools/chpl-language-server/src/chpl-shim.py +++ b/tools/chpl-language-server/src/chpl-shim.py @@ -28,6 +28,7 @@ from chapel import * from chapel.core import * + def list_parsed_files(files, module_paths): ctx = Context() ctx.set_module_paths(module_paths, files) @@ -56,7 +57,11 @@ def run_toplevel(): os.symlink(__file__, wrapper) # Modify path to include the wrapper script at the front - chpl = subprocess.check_output(["which", "chpl"]).decode(sys.stdout.encoding).strip() + chpl = ( + subprocess.check_output(["which", "chpl"]) + .decode(sys.stdout.encoding) + .strip() + ) newpath = os.pathsep.join([tempdir.name, os.environ["PATH"]]) newenv = os.environ.copy() @@ -89,8 +94,10 @@ def run_chpl_shim(): tmpfile_path = os.environ["CHPL_SHIM_TARGET_PATH"] parser = argparse.ArgumentParser() - parser.add_argument('filename', nargs='*') - parser.add_argument('-M', '--module-dir', action='append', dest="module_dirs", default=[]) + parser.add_argument("filename", nargs="*") + parser.add_argument( + "-M", "--module-dir", action="append", dest="module_dirs", default=[] + ) args, _ = parser.parse_known_args() files = args.filename @@ -109,12 +116,16 @@ def run_chpl_shim(): for file in files: invocation = " ".join(sys.argv).replace(__file__, real_compiler) commands[file] = { - # "invocation": invocation, - "module_dirs": [os.path.abspath(mod_dir) for mod_dir in args.module_dirs], + # "invocation": invocation, + "module_dirs": [ + os.path.abspath(mod_dir) for mod_dir in args.module_dirs + ], "files": files, } - tmpfile = tempfile.NamedTemporaryFile(mode="w", delete=False, dir=tmpfile_path, suffix=".json") + tmpfile = tempfile.NamedTemporaryFile( + mode="w", delete=False, dir=tmpfile_path, suffix=".json" + ) # Convert to json with tmpfile as f: diff --git a/tools/chpl-language-server/src/symbol_signature.py b/tools/chpl-language-server/src/symbol_signature.py index 6937edf8716a..4fad9d15778e 100644 --- a/tools/chpl-language-server/src/symbol_signature.py +++ b/tools/chpl-language-server/src/symbol_signature.py @@ -256,11 +256,12 @@ def _proc_to_string(node: chapel.Function) -> List[Component]: if node.is_inline(): comps.append(_wrap_str("inline ")) - comps.append(_wrap_str(f"{node.kind()} ")) # if it has a this-formal, check for this intent if node.this_formal() and _intent_to_string(node.this_formal().intent()): - comps.append(_wrap_str(f"{_intent_to_string(node.this_formal().intent())} ")) + comps.append( + _wrap_str(f"{_intent_to_string(node.this_formal().intent())} ") + ) comps.append(_wrap_str(node.name())) if not node.is_parenless(): diff --git a/tools/chplcheck/src/chplcheck.py b/tools/chplcheck/src/chplcheck.py index 459c2dd3eb48..0cf0bcdffb65 100755 --- a/tools/chplcheck/src/chplcheck.py +++ b/tools/chplcheck/src/chplcheck.py @@ -67,9 +67,21 @@ def load_module(driver: LintDriver, file_path: str): def int_to_nth(n: int) -> str: - d = {1: "first", 2: "second", 3: "third", 4: "fourth", 5: "fifth", 6: "sixth", 7: "seventh", 8: "eighth", 9: "ninth", 10: "tenth"} + d = { + 1: "first", + 2: "second", + 3: "third", + 4: "fourth", + 5: "fifth", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", + 10: "tenth", + } return d.get(n, f"{n}th") + def apply_fixits( violations: List[Tuple[chapel.AstNode, str, Optional[List[Fixit]]]], suffix: Optional[str], @@ -114,7 +126,9 @@ def apply_fixits( edits_to_apply.extend(fixit.edits) done = True except (ValueError, IndexError): - print("Please enter a number corresponding to an option") + print( + "Please enter a number corresponding to an option" + ) except KeyboardInterrupt: # apply no edits, return the original violations return violations @@ -183,19 +197,60 @@ def print_rules(driver: LintDriver, show_all=True): def main(): - parser = argparse.ArgumentParser(prog="chplcheck", description="A linter for the Chapel language") + parser = argparse.ArgumentParser( + prog="chplcheck", description="A linter for the Chapel language" + ) parser.add_argument("filenames", nargs="*") - parser.add_argument("--disable-rule", action="append", dest="disabled_rules", default=[]) - parser.add_argument("--enable-rule", action="append", dest="enabled_rules", default=[]) + parser.add_argument( + "--disable-rule", action="append", dest="disabled_rules", default=[] + ) + parser.add_argument( + "--enable-rule", action="append", dest="enabled_rules", default=[] + ) parser.add_argument("--lsp", action="store_true", default=False) parser.add_argument("--skip-unstable", action="store_true", default=False) - parser.add_argument("--internal-prefix", action="append", dest="internal_prefixes", default=[]) - parser.add_argument("--add-rules", action="append", default=[], help="Add a custom rule file") - parser.add_argument("--list-rules", action="store_true", default=False, help="List all available rules") - parser.add_argument("--list-active-rules", action="store_true", default=False, help="List all currently enabled rules") - parser.add_argument("--fixit", action="store_true", default=False, help="Apply fixits for the relevant rules") - parser.add_argument("--fixit-suffix", default=None, help="Suffix to append to the original file name when applying fixits. If not set (the default), the original file will be overwritten.") - parser.add_argument("--interactive", "-i", action="store_true", default=False, help="Apply fixits interactively, requires --fixit") + parser.add_argument( + "--internal-prefix", + action="append", + dest="internal_prefixes", + default=[], + ) + parser.add_argument( + "--add-rules", + action="append", + default=[], + help="Add a custom rule file", + ) + parser.add_argument( + "--list-rules", + action="store_true", + default=False, + help="List all available rules", + ) + parser.add_argument( + "--list-active-rules", + action="store_true", + default=False, + help="List all currently enabled rules", + ) + parser.add_argument( + "--fixit", + action="store_true", + default=False, + help="Apply fixits for the relevant rules", + ) + parser.add_argument( + "--fixit-suffix", + default=None, + help="Suffix to append to the original file name when applying fixits. If not set (the default), the original file will be overwritten.", + ) + parser.add_argument( + "--interactive", + "-i", + action="store_true", + default=False, + help="Apply fixits interactively, requires --fixit", + ) args = parser.parse_args() driver = LintDriver( @@ -238,7 +293,9 @@ def main(): violations.sort(key=lambda f: f[0].location().start()[0]) if args.fixit: - violations = apply_fixits(violations, args.fixit_suffix, args.interactive) + violations = apply_fixits( + violations, args.fixit_suffix, args.interactive + ) for node, rule, _ in violations: print_violation(node, rule) diff --git a/tools/chplcheck/src/fixits.py b/tools/chplcheck/src/fixits.py index 6335cda82271..aa9d55b92326 100644 --- a/tools/chplcheck/src/fixits.py +++ b/tools/chplcheck/src/fixits.py @@ -35,9 +35,7 @@ class Edit: @classmethod def build(cls, location: chapel.Location, text: str) -> "Edit": - return Edit( - location.path(), location.start(), location.end(), text - ) + return Edit(location.path(), location.start(), location.end(), text) @classmethod def to_dict(cls, fixit: "Edit") -> typing.Dict: diff --git a/tools/chplcheck/src/lsp.py b/tools/chplcheck/src/lsp.py index 34b4fd49303a..e7beff937455 100644 --- a/tools/chplcheck/src/lsp.py +++ b/tools/chplcheck/src/lsp.py @@ -152,7 +152,9 @@ async def code_action(ls: LanguageServer, params: CodeActionParams): start = e.start end = e.end rng = Range( - start=Position(max(start[0] - 1, 0), max(start[1] - 1, 0)), + start=Position( + max(start[0] - 1, 0), max(start[1] - 1, 0) + ), end=Position(max(end[0] - 1, 0), max(end[1] - 1, 0)), ) edit = TextEdit(range=rng, new_text=e.text) diff --git a/tools/chplcheck/src/rule_types.py b/tools/chplcheck/src/rule_types.py index 6575dca73c0a..03a2f0172f8c 100644 --- a/tools/chplcheck/src/rule_types.py +++ b/tools/chplcheck/src/rule_types.py @@ -22,23 +22,33 @@ import chapel from fixits import Fixit, Edit -def _build_ignore_fixit(anchor: chapel.AstNode, lines: typing.List[str], rule_name: str) -> Fixit: + +def _build_ignore_fixit( + anchor: chapel.AstNode, lines: typing.List[str], rule_name: str +) -> Fixit: # TODO: how should this handle multiple ignores? loc = anchor.location() text = chapel.range_to_text(loc, lines) indent_amount = max(loc.start()[1] - 1, 0) - indent = " "* indent_amount + indent = " " * indent_amount text = f'@chplcheck.ignore("{rule_name}")\n' + indent + text ignore = Fixit.build(Edit.build(loc, text)) ignore.description = "Ignore this warning" return ignore + class BasicRuleResult: """ Result type for basic rules. Rules can also return a plain boolean to represent a simple pass/fail result, with no fixit. """ - def __init__(self, node: chapel.AstNode, ignorable: bool = False, fixits: typing.Optional[typing.Union[Fixit, typing.List[Fixit]]] = None): + + def __init__( + self, + node: chapel.AstNode, + ignorable: bool = False, + fixits: typing.Optional[typing.Union[Fixit, typing.List[Fixit]]] = None, + ): self.node = node self.ignorable = ignorable if fixits is None: @@ -60,7 +70,6 @@ def fixits(self, context: chapel.Context, name: str) -> typing.List[Fixit]: return to_return - _BasicRuleResult = typing.Union[bool, BasicRuleResult] """Internal type for basic rule results""" BasicRule = typing.Callable[[chapel.Context, chapel.AstNode], _BasicRuleResult] @@ -72,7 +81,13 @@ class AdvancedRuleResult: Result type for advanced rules. Advanced rules can also return a plain boolean to represent a simple pass/fail result, with no fixit. Having an anchor implies that it is ignorable """ - def __init__(self, node: chapel.AstNode, anchor: typing.Optional[chapel.AstNode] = None, fixits: typing.Optional[typing.Union[Fixit, typing.List[Fixit]]] = None): + + def __init__( + self, + node: chapel.AstNode, + anchor: typing.Optional[chapel.AstNode] = None, + fixits: typing.Optional[typing.Union[Fixit, typing.List[Fixit]]] = None, + ): self.node = node self.anchor = anchor if fixits is None: diff --git a/tools/chplcheck/src/rules.py b/tools/chplcheck/src/rules.py index 78229265e350..9d037765ae7c 100644 --- a/tools/chplcheck/src/rules.py +++ b/tools/chplcheck/src/rules.py @@ -26,6 +26,7 @@ from fixits import Fixit, Edit from rule_types import BasicRuleResult, AdvancedRuleResult + def variables(node: AstNode): if isinstance(node, Variable): if node.name() != "_": @@ -34,7 +35,12 @@ def variables(node: AstNode): for child in node: yield from variables(child) -def fixit_remove_unused_node(node: AstNode, lines: Optional[List[str]] = None, context: Optional[Context] = None) -> Optional[Fixit]: + +def fixit_remove_unused_node( + node: AstNode, + lines: Optional[List[str]] = None, + context: Optional[Context] = None, +) -> Optional[Fixit]: """ Given an unused variable that's either a child of a TupleDecl or an iterand in a loop, construct a Fixit that removes it or replaces it @@ -67,6 +73,7 @@ def fixit_remove_unused_node(node: AstNode, lines: Optional[List[str]] = None, c return Fixit.build(Edit.build(loc, before_lines + after_lines)) return None + def name_for_linting(context: Context, node: NamedDecl) -> str: name = node.name() @@ -89,6 +96,7 @@ def check_pascal_case(context: Context, node: NamedDecl): r"(([A-Z][a-z]*|\d+)+|[A-Z]+)?", name_for_linting(context, node) ) + def register_rules(driver: LintDriver): @driver.basic_rule(VarLikeDecl, default=False) def CamelOrPascalCaseVariables(context: Context, node: VarLikeDecl): @@ -232,7 +240,9 @@ def BoolLitInCondStmt(context: Context, node: Conditional): # should be set in all branches assert text is not None - return BasicRuleResult(node, fixits=Fixit.build(Edit.build(node.location(), text))) + return BasicRuleResult( + node, fixits=Fixit.build(Edit.build(node.location(), text)) + ) @driver.basic_rule(NamedDecl) def ChplPrefixReserved(context: Context, node: NamedDecl): @@ -270,7 +280,9 @@ def EmptyStmts(_, node: EmptyStmt): # dont warn if the EmptyStmt is the only statement in a block return True - return BasicRuleResult(node, fixits=Fixit.build(Edit.build(node.location(), ""))) + return BasicRuleResult( + node, fixits=Fixit.build(Edit.build(node.location(), "")) + ) @driver.basic_rule(TupleDecl) def UnusedTupleUnpack(context: Context, node: TupleDecl): @@ -280,7 +292,7 @@ def UnusedTupleUnpack(context: Context, node: TupleDecl): varset = set(variables(node)) if len(varset) == 0: - fixit = fixit_remove_unused_node(node, context = context) + fixit = fixit_remove_unused_node(node, context=context) if fixit is not None: return BasicRuleResult(node, fixits=fixit) return False @@ -563,7 +575,7 @@ def contains_statements(node: AstNode) -> bool: Interface, Union, Enum, - Cobegin + Cobegin, ) return isinstance(node, classes) @@ -603,7 +615,10 @@ def unwrap_intermediate_block(node: AstNode) -> Optional[AstNode]: # For implicit modules, proper code will technically be on the same # line as the module's body. But we don't want to warn about that, # since we don't want to ask all code to be indented one level deeper. - elif not (isinstance(parent_for_indentation, Module) and parent_for_indentation.kind() == "implicit"): + elif not ( + isinstance(parent_for_indentation, Module) + and parent_for_indentation.kind() == "implicit" + ): parent_depth = parent_for_indentation.location().start()[1] prev = None @@ -619,7 +634,8 @@ def unwrap_intermediate_block(node: AstNode) -> Optional[AstNode]: iterable = root.stmts() for child in iterable: - if isinstance(child, Comment): continue + if isinstance(child, Comment): + continue # some NamedDecl nodes currently use the name as the location, which # does not indicate their actual indentation. @@ -653,7 +669,11 @@ def unwrap_intermediate_block(node: AstNode) -> Optional[AstNode]: elif prev_depth and depth != prev_depth: # Special case, slightly coarse: avoid double-warning with # MisleadingIndentation - if not(prev and isinstance(prev, Loop) and prev.block_style() == "implicit"): + if not ( + prev + and isinstance(prev, Loop) + and prev.block_style() == "implicit" + ): yield child # Do not update 'prev_depth'; use original prev_depth as