From 1f11c185c0541d419ada74fb938f4ac78f122971 Mon Sep 17 00:00:00 2001 From: Jimmy Lai Date: Fri, 2 Oct 2020 19:09:52 -0700 Subject: [PATCH] add AddMissingHeaderRule to check copyright header comments (#142) * add AddMissingHeaderRule to check copyright header comments * remove unused import --- .fixit.config.yaml | 7 ++ fixit/cli/tests/__init__.py | 4 + fixit/common/tests/__init__.py | 4 + fixit/rules/add_file_header.py | 141 +++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 fixit/rules/add_file_header.py diff --git a/.fixit.config.yaml b/.fixit.config.yaml index 1cbcba48..cd8e6626 100644 --- a/.fixit.config.yaml +++ b/.fixit.config.yaml @@ -13,3 +13,10 @@ rule_config: ImportConstraintsRule: fixit: rules: [["*", "allow"]] + AddMissingHeaderRule: + path: "*.py" + header: |- + # Copyright (c) Facebook, Inc. and its affiliates. + # + # This source code is licensed under the MIT license found in the + # LICENSE file in the root directory of this source tree. diff --git a/fixit/cli/tests/__init__.py b/fixit/cli/tests/__init__.py index e69de29b..62642369 100644 --- a/fixit/cli/tests/__init__.py +++ b/fixit/cli/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. diff --git a/fixit/common/tests/__init__.py b/fixit/common/tests/__init__.py index e69de29b..62642369 100644 --- a/fixit/common/tests/__init__.py +++ b/fixit/common/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. diff --git a/fixit/rules/add_file_header.py b/fixit/rules/add_file_header.py new file mode 100644 index 00000000..99b7ef72 --- /dev/null +++ b/fixit/rules/add_file_header.py @@ -0,0 +1,141 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List + +import libcst as cst +import libcst.matchers as m + +from fixit import ( + CstContext, + CstLintRule, + InvalidTestCase as Invalid, + LintConfig, + ValidTestCase as Valid, +) + + +class AddMissingHeaderRule(CstLintRule): + """ + Verify if required header comments exist in a module. + Configuration is required in ``.fixit.config.yaml`` in order to enable this rule:: + + rule_config: + AddMissingHeaderRule: + path: pkg/*.py + header: |- + # header line 1 + # header line 2 + + (Use ``|-`` to keep newlines and add no new line at the end of header comments.) + ``path`` is a glob-style ``str`` used in `Path.match() `_. + The specified ``header`` is a newline-separated ``str`` to be enforced in files whose path matches. + """ + + MESSAGE: str = "A required header comment for this file is missing." + + VALID = [ + Valid("import libcst"), + Valid( + """ + # header line 1 + # header line 2 + import libcst + """, + config=LintConfig( + rule_config={ + "AddMissingHeaderRule": { + "path": "*.py", + "header": "# header line 1\n# header line 2", + } + } + ), + ), + Valid( + """ + # header line 1 + # header line 2 + # An extra header line is ok + import libcst + """, + config=LintConfig( + rule_config={ + "AddMissingHeaderRule": { + "path": "*.py", + "header": "# header line 1\n# header line 2", + } + } + ), + ), + Valid( + """ + # other header in an unrelated file + import libcst + """, + filename="b/m.py", + config=LintConfig( + rule_config={ + "AddMissingHeaderRule": { + "path": "a/*.py", + "header": "# header line 1\n# header line 2", + } + } + ), + ), + ] + INVALID = [ + Invalid( + "# wrong header", + config=LintConfig( + rule_config={ + "AddMissingHeaderRule": {"path": "*.py", "header": "# header line"} + } + ), + expected_replacement="# header line\n# wrong header", + ) + ] + + def __init__(self, context: CstContext) -> None: + super().__init__(context) + config = self.context.config.rule_config.get(self.__class__.__name__, None) + if config is None: + self.rule_disabled: bool = True + else: + if not isinstance(config, dict) or "header" not in config: + raise ValueError( + "A ``header`` str config is required by AddMissingHeaderRule." + ) + header_str = config["header"] + if not isinstance(header_str, str): + raise ValueError( + "A ``header`` str config is required by AddMissingHeaderRule." + ) + lines = header_str.split("\n") + self.header_matcher: List[m.EmptyLine] = [ + m.EmptyLine(comment=m.Comment(value=line)) for line in lines + ] + self.header_replacement: List[cst.EmptyLine] = [ + cst.EmptyLine(comment=cst.Comment(value=line)) for line in lines + ] + if "path" in config: + path_pattern = config["path"] + if not isinstance(path_pattern, str): + raise ValueError( + "``path`` config should be a str in AddMissingHeaderRule." + ) + else: + path_pattern = "*.py" + self.rule_disabled = not self.context.file_path.match(path_pattern) + + def visit_Module(self, node: cst.Module) -> None: + if self.rule_disabled: + return + if not m.matches(node, m.Module(header=[*self.header_matcher, m.ZeroOrMore()])): + self.report( + node, + replacement=node.with_changes( + header=[*self.header_replacement, *node.header] + ), + )