/
base.py
153 lines (123 loc) · 4.89 KB
/
base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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.
import re
from abc import ABCMeta
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
import libcst as cst
from libcst import BatchableCSTVisitor
from libcst.metadata import (
BaseMetadataProvider,
CodePosition,
MetadataWrapper,
PositionProvider,
TypeInferenceProvider,
)
from fixit.common.report import BaseLintRuleReport, CstLintRuleReport
if TYPE_CHECKING:
from fixit.common.pseudo_rule import PseudoLintRule
from libcst.metadata.base_provider import ProviderT
LintRuleT = Union[Type["CstLintRule"], Type["PseudoLintRule"]]
CACHE_DEPENDENT_PROVIDERS: Tuple["ProviderT"] = (TypeInferenceProvider,)
def _get_code(message: str) -> str:
"""Extract the lint code from the beginning of the lint message."""
# TODO: This shouldn't really exist, and we should treat lint codes and messages as
# separate concepts.
code_match = re.match(r"^(?P<code>IG\d+) \S", message)
if not code_match:
raise ValueError(
"Report messages should begin with IGXX, where XX is the number "
+ "associated with the rule, followed by a single space."
)
return code_match.group("code")
DEFAULT_PACKAGES = ["fixit.rules"]
DEFAULT_PATTERNS = [f"@ge{''}nerated", "@nolint"]
@dataclass(frozen=True)
class LintConfig:
block_list_patterns: List[str] = field(default_factory=lambda: DEFAULT_PATTERNS)
block_list_rules: List[str] = field(default_factory=list)
fixture_dir: str = "./fixtures"
formatter: List[str] = field(default_factory=list)
packages: List[str] = field(default_factory=lambda: DEFAULT_PACKAGES)
repo_root: str = "."
rule_config: Dict[str, Dict[str, object]] = field(default_factory=dict)
class BaseContext:
file_path: Path
config: LintConfig
reports: List[BaseLintRuleReport]
def __init__(self, file_path: Path, config: LintConfig) -> None:
self.file_path = file_path
self.config = config
self.reports = []
@property
def in_tests(self) -> bool:
return self.file_path.name == "tests.py" or "tests" in self.file_path.parts
@property
def in_scripts(self) -> bool:
return Path("distillery/scripts") in self.file_path.parents
class CstContext(BaseContext):
wrapper: MetadataWrapper
_source: bytes
node_stack: List[cst.CSTNode]
def __init__(
self,
wrapper: MetadataWrapper,
source: bytes,
file_path: Path,
config: LintConfig,
) -> None:
super().__init__(file_path, config)
self.wrapper = wrapper
# Keep the source around so we can use it in autofix diff generation. This is
# private because lint rules should use the CST tree, not the source code. If we
# exposed the source, it'd be providing rope for people to hang themselves with.
self._source = source
self.node_stack = []
class CstLintRule(BatchableCSTVisitor, metaclass=ABCMeta):
#: a short message in one or two sentences show to user when the rule is violated.
MESSAGE: Optional[str] = None
METADATA_DEPENDENCIES: Tuple[Type[BaseMetadataProvider], ...] = (PositionProvider,)
def __init__(self, context: CstContext) -> None:
super().__init__()
self.context = context
def should_skip_file(self) -> bool:
return False
def report(
self,
node: cst.CSTNode,
message: Optional[str] = None,
*,
position: Optional[CodePosition] = None,
replacement: Optional[Union[cst.CSTNode, cst.RemovalSentinel]] = None,
) -> None:
"""
Report a lint violation for a given node. Optionally specify a custom
position to report an error at or a replacement node for an auto-fix.
"""
if position is None:
position = self.context.wrapper.resolve(PositionProvider)[node].start
if message is None:
message = self.MESSAGE
if message is None:
raise Exception(f"No lint message was provided to rule: {self}")
report = CstLintRuleReport(
file_path=self.context.file_path,
node=node,
code=_get_code(message),
message=message.split(" ", 1)[1],
line=position.line,
# libcst columns are 0-indexed but arc is 1-indexed
column=(position.column + 1),
module=self.context.wrapper,
module_bytes=self.context._source,
replacement_node=replacement,
)
self.context.reports.append(report)
@classmethod
def requires_metadata_caches(cls) -> bool:
return any(
p in CACHE_DEPENDENT_PROVIDERS for p in cls.get_inherited_dependencies()
)