blob: b43654d30e3c33cb83c9186d786243d47517a287 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2023 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import ast
import dataclasses
import json
import os
@dataclasses.dataclass
class Finding:
message: str
line: int
col: int = 0
end_line: int = 0
end_col: int = 0
replacements: list = ()
level: str = "error"
def main():
parser = argparse.ArgumentParser(description="Enforce the recipe style guide")
parser.add_argument("filename", help="Name of the file to check.")
args = parser.parse_args()
with open(args.filename) as f:
tree = ast.parse(f.read(), filename=args.filename)
findings = []
if is_recipe_file(args.filename):
findings.extend(check_recipe_function_naming(tree))
findings.extend(check_recipe_function_ordering(tree))
print(json.dumps([dataclasses.asdict(f) for f in findings], indent=2))
def is_recipe_file(path):
if not path.endswith(".py"):
return False
path_parts = path.split(os.sep)
if any(p.endswith(".resources") for p in path_parts):
return False
if path_parts[0] == "recipes":
return True
if path_parts[0] == "recipe_modules" and path_parts[-2] in ("tests", "examples"):
return True
return False
def check_recipe_function_naming(tree: ast.AST):
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name.startswith("_"):
# node.col_offset is the column of the "def" keyword, but it's nicer
# if we only highlight the function name instead of the whole "def"
# line. Note that this is fragile, as it assumes only one space
# between the "def" keyword and the function name, which may not be
# the case if the code has not been auto-formatted.
col = node.col_offset + len("def ")
yield Finding(
line=node.lineno,
end_line=node.lineno,
col=col,
end_col=col + len(node.name),
message=f"{node.name} should not start with an underscore",
replacements=[node.name.lstrip("_")],
)
def check_recipe_function_ordering(tree: ast.AST):
function_defs = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
for index, node in enumerate(function_defs):
if node.name == "RunSteps" and index != 0:
yield Finding(
line=node.lineno,
end_line=node.lineno,
message=f"{node.name} should be the first function in a recipe",
level="warning",
)
if node.name == "GenTests" and index != len(function_defs) - 1:
yield Finding(
line=node.lineno,
end_line=node.lineno,
message=f"{node.name} should be the last function in a recipe",
level="warning",
)
if __name__ == "__main__":
main()