| # 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. |
| |
| """Checks a shac Starlark file for fuchsia-specific issues. |
| |
| Since Starlark's syntax is a subset of Python's, we can use the Python ast |
| library to parse Starlark. |
| """ |
| |
| import ast |
| import collections |
| import json |
| import sys |
| |
| |
| COMMON_DOT_STAR = "scripts/shac/common.star" |
| |
| # Add a comment of this form after the closing parenthesis of a `print()` call |
| # to allow it to be committed. |
| ALLOW_PRINT_DIRECTIVE = "# allow-print" |
| |
| |
| def main(): |
| findings = collections.defaultdict(list) |
| path = sys.argv[1] |
| with open(path) as f: |
| contents = f.read() |
| lines = contents.splitlines() |
| |
| tree = ast.parse(contents) |
| for node in ast.walk(tree): |
| if ( |
| isinstance(node, ast.Call) |
| and ast.unparse(node.func) == "ctx.emit.finding" |
| and not any(kw.arg == "message" for kw in node.keywords) |
| ): |
| # shac requires `message` except for findings emitted by formatters, |
| # in which case it provides a default message saying to run `shac |
| # fmt`. However, `shac fmt` isn't exposed to fuchsia developers |
| # directly, instead they should use `fx format-code`, so we should |
| # never fall back to the default message. |
| findings[node].append( |
| "`message` argument to `ctx.emit.finding()` must be set." |
| ) |
| |
| if ( |
| isinstance(node, ast.Call) |
| and ast.unparse(node.func) == "print" |
| and not lines[node.end_lineno - 1].endswith(ALLOW_PRINT_DIRECTIVE) |
| ): |
| findings[node].append( |
| "Do not commit shac check code that calls `print()`, " |
| "it's only to be used for debugging." |
| ) |
| |
| if ( |
| isinstance(node, ast.Call) |
| and ast.unparse(node.func) == "ctx.os.exec" |
| and path != COMMON_DOT_STAR |
| ): |
| # All paths passed to `os_exec()` must be absolute in order for |
| # inherited checks in //vendor repositories to work. It's nontrivial |
| # to validate that statically, so instead we validate it at runtime |
| # in a custom wrapper function, and statically enforce that that |
| # wrapper function is used instead of `os_exec()`. |
| findings[node].append( |
| f"Don't call `ctx.os.exec()` directly, call `os_exec()` " |
| f"from //{COMMON_DOT_STAR}." |
| ) |
| |
| print( |
| json.dumps( |
| [ |
| { |
| "message": msg, |
| "line": node.lineno, |
| "col": node.col_offset + 1, |
| "end_line": node.end_lineno, |
| "end_col": node.end_col_offset + 1, |
| } |
| for node, msgs in findings.items() |
| for msg in msgs |
| ], |
| indent=2, |
| ) |
| ) |
| |
| |
| if __name__ == "__main__": |
| main() |