pythaw 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pythaw-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythaw
3
+ Version: 0.1.0
4
+ Summary: A Python static analysis tool that detects heavy initialization inside AWS Lambda handlers that should be moved to module scope for faster warm starts.
5
+ Keywords: aws,lambda,static-analysis,boto3,linter
6
+ Author: MiuraToya
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: rich>=14.0.0
21
+ Requires-Dist: tomli>=2.0.0 ; python_full_version < '3.11'
22
+ Requires-Python: >=3.10
23
+ Project-URL: Repository, https://github.com/MiuraToya/pythaw
24
+ Description-Content-Type: text/markdown
25
+
26
+ # pythaw
27
+
28
+ [日本語ドキュメント](README.ja.md)
29
+
30
+ A Python static analysis tool that detects heavy initialization inside AWS Lambda handlers that should be moved to module scope for faster warm starts.
31
+
32
+ Recursively follows function calls—including across imported files—to catch indirect violations.
33
+
34
+ ## Requirements
35
+
36
+ Python 3.10 - 3.14 — matching the actively supported AWS Lambda Python runtimes.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ # pip
42
+ pip install pythaw
43
+
44
+ # uv
45
+ uv add pythaw
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ # handler.py
52
+
53
+ def lambda_handler(event, context):
54
+ # BAD: Creating a boto3 client inside the handler
55
+ # runs initialization on every invocation,
56
+ # losing the benefit of warm starts.
57
+ client = boto3.client("s3")
58
+ return client.get_object(Bucket="my-bucket", Key=event["key"])
59
+ ```
60
+
61
+ ```
62
+ $ pythaw check handler.py
63
+ infra/aws.py:7:15: PW001 boto3.client() should be called at module scope
64
+ → handler.py:5:11 process()
65
+ → service.py:5:13 S3Provider.get_client()
66
+
67
+ Found 1 violation in 1 file.
68
+ ```
69
+
70
+ Move the client to module scope so Lambda container reuse skips the initialization:
71
+
72
+ ```python
73
+ # handler.py (fixed)
74
+
75
+ client = boto3.client("s3")
76
+
77
+ def lambda_handler(event, context):
78
+ return client.get_object(Bucket="my-bucket", Key=event["key"])
79
+ ```
80
+
81
+ ```
82
+ $ pythaw check handler.py
83
+ All checks passed!
84
+ ```
85
+
86
+ ## Usage
87
+
88
+ ```bash
89
+ pythaw check <path> # Check a file or directory
90
+ pythaw check . --format json # JSON output
91
+ pythaw check . --format github # GitHub Actions annotation format
92
+ pythaw check . --format sarif # SARIF format (Code Scanning integration)
93
+ pythaw check . --select PW001,PW002 # Enable only specific rules
94
+ pythaw check . --ignore PW003 # Disable specific rules
95
+ pythaw check . --exit-zero # Always exit with code 0
96
+ pythaw check . --statistics # Show per-rule violation counts
97
+ pythaw rules # List built-in rules
98
+ pythaw rule PW001 # Show rule details
99
+ ```
100
+
101
+ ### Exit Codes
102
+
103
+ | Code | Meaning |
104
+ |------|---------|
105
+ | 0 | No violations found |
106
+ | 1 | Violations found |
107
+ | 2 | Tool error (invalid config, etc.) |
108
+
109
+ ## Rules
110
+
111
+ | ID | Detects |
112
+ |-------|---------|
113
+ | PW001 | `boto3.client()` |
114
+ | PW002 | `boto3.resource()` |
115
+ | PW003 | `boto3.Session()` |
116
+ | PW004 | `pymysql.connect()` |
117
+ | PW005 | `psycopg2.connect()` |
118
+ | PW006 | `redis.Redis()` |
119
+ | PW007 | `redis.StrictRedis()` |
120
+ | PW008 | `httpx.Client()` |
121
+ | PW009 | `requests.Session()` |
122
+
123
+ ## Call Graph Traversal
124
+
125
+ pythaw recursively follows local function calls and imported modules from the handler, detecting indirect violations across files.
126
+
127
+ ### Supported patterns
128
+
129
+ | Pattern | Example |
130
+ |---------|---------|
131
+ | Same-file function call | `helper()` |
132
+ | Module-qualified function call | `infra.get_client()` |
133
+ | Class method call | `AwsProvider.get_client()` |
134
+ | Class instantiation (`__init__`) | `S3Client()` |
135
+ | Cross-file import tracking | `from infra import get_client` |
136
+
137
+ > **Note:** Instance method calls via variables (e.g. `obj = Cls(); obj.method()`) are not tracked — this would require data-flow analysis beyond the current scope.
138
+
139
+ ```
140
+ infra/aws.py:4:15: PW001 boto3.client() should be called at module scope
141
+ → handler.py:2:10 get_client()
142
+
143
+ Found 1 violation in 1 file.
144
+ ```
145
+
146
+ ## Suppression
147
+
148
+ ### Inline suppression
149
+
150
+ Append `# nopw: <code>` to a line to suppress that violation. Multiple codes can be comma-separated.
151
+
152
+ ```python
153
+ client = boto3.client("s3") # nopw: PW001
154
+ ```
155
+
156
+ ### File-level suppression
157
+
158
+ Add `# pythaw: nocheck` in the leading comment block to skip the entire file.
159
+
160
+ ```python
161
+ # pythaw: nocheck
162
+ import boto3
163
+
164
+ def handler(event, context):
165
+ boto3.client("s3") # not checked
166
+ ```
167
+
168
+ ## Configuration
169
+
170
+ Configure via the `[tool.pythaw]` section in `pyproject.toml`.
171
+
172
+ ```toml
173
+ [tool.pythaw]
174
+ # Function name patterns recognized as handlers (fnmatch syntax)
175
+ handler_patterns = ["handler", "lambda_handler", "*_handler"]
176
+
177
+ # Patterns to exclude from scanning
178
+ exclude = [".venv", "tests"]
179
+
180
+ # Disable specific rules per file pattern
181
+ [tool.pythaw.per-file-ignores]
182
+ "tests/*" = ["PW001", "PW002"]
183
+ "scripts/*" = ["PW001"]
184
+ ```
185
+
186
+ ## License
187
+
188
+ [MIT](LICENSE)
pythaw-0.1.0/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # pythaw
2
+
3
+ [日本語ドキュメント](README.ja.md)
4
+
5
+ A Python static analysis tool that detects heavy initialization inside AWS Lambda handlers that should be moved to module scope for faster warm starts.
6
+
7
+ Recursively follows function calls—including across imported files—to catch indirect violations.
8
+
9
+ ## Requirements
10
+
11
+ Python 3.10 - 3.14 — matching the actively supported AWS Lambda Python runtimes.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ # pip
17
+ pip install pythaw
18
+
19
+ # uv
20
+ uv add pythaw
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```python
26
+ # handler.py
27
+
28
+ def lambda_handler(event, context):
29
+ # BAD: Creating a boto3 client inside the handler
30
+ # runs initialization on every invocation,
31
+ # losing the benefit of warm starts.
32
+ client = boto3.client("s3")
33
+ return client.get_object(Bucket="my-bucket", Key=event["key"])
34
+ ```
35
+
36
+ ```
37
+ $ pythaw check handler.py
38
+ infra/aws.py:7:15: PW001 boto3.client() should be called at module scope
39
+ → handler.py:5:11 process()
40
+ → service.py:5:13 S3Provider.get_client()
41
+
42
+ Found 1 violation in 1 file.
43
+ ```
44
+
45
+ Move the client to module scope so Lambda container reuse skips the initialization:
46
+
47
+ ```python
48
+ # handler.py (fixed)
49
+
50
+ client = boto3.client("s3")
51
+
52
+ def lambda_handler(event, context):
53
+ return client.get_object(Bucket="my-bucket", Key=event["key"])
54
+ ```
55
+
56
+ ```
57
+ $ pythaw check handler.py
58
+ All checks passed!
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ```bash
64
+ pythaw check <path> # Check a file or directory
65
+ pythaw check . --format json # JSON output
66
+ pythaw check . --format github # GitHub Actions annotation format
67
+ pythaw check . --format sarif # SARIF format (Code Scanning integration)
68
+ pythaw check . --select PW001,PW002 # Enable only specific rules
69
+ pythaw check . --ignore PW003 # Disable specific rules
70
+ pythaw check . --exit-zero # Always exit with code 0
71
+ pythaw check . --statistics # Show per-rule violation counts
72
+ pythaw rules # List built-in rules
73
+ pythaw rule PW001 # Show rule details
74
+ ```
75
+
76
+ ### Exit Codes
77
+
78
+ | Code | Meaning |
79
+ |------|---------|
80
+ | 0 | No violations found |
81
+ | 1 | Violations found |
82
+ | 2 | Tool error (invalid config, etc.) |
83
+
84
+ ## Rules
85
+
86
+ | ID | Detects |
87
+ |-------|---------|
88
+ | PW001 | `boto3.client()` |
89
+ | PW002 | `boto3.resource()` |
90
+ | PW003 | `boto3.Session()` |
91
+ | PW004 | `pymysql.connect()` |
92
+ | PW005 | `psycopg2.connect()` |
93
+ | PW006 | `redis.Redis()` |
94
+ | PW007 | `redis.StrictRedis()` |
95
+ | PW008 | `httpx.Client()` |
96
+ | PW009 | `requests.Session()` |
97
+
98
+ ## Call Graph Traversal
99
+
100
+ pythaw recursively follows local function calls and imported modules from the handler, detecting indirect violations across files.
101
+
102
+ ### Supported patterns
103
+
104
+ | Pattern | Example |
105
+ |---------|---------|
106
+ | Same-file function call | `helper()` |
107
+ | Module-qualified function call | `infra.get_client()` |
108
+ | Class method call | `AwsProvider.get_client()` |
109
+ | Class instantiation (`__init__`) | `S3Client()` |
110
+ | Cross-file import tracking | `from infra import get_client` |
111
+
112
+ > **Note:** Instance method calls via variables (e.g. `obj = Cls(); obj.method()`) are not tracked — this would require data-flow analysis beyond the current scope.
113
+
114
+ ```
115
+ infra/aws.py:4:15: PW001 boto3.client() should be called at module scope
116
+ → handler.py:2:10 get_client()
117
+
118
+ Found 1 violation in 1 file.
119
+ ```
120
+
121
+ ## Suppression
122
+
123
+ ### Inline suppression
124
+
125
+ Append `# nopw: <code>` to a line to suppress that violation. Multiple codes can be comma-separated.
126
+
127
+ ```python
128
+ client = boto3.client("s3") # nopw: PW001
129
+ ```
130
+
131
+ ### File-level suppression
132
+
133
+ Add `# pythaw: nocheck` in the leading comment block to skip the entire file.
134
+
135
+ ```python
136
+ # pythaw: nocheck
137
+ import boto3
138
+
139
+ def handler(event, context):
140
+ boto3.client("s3") # not checked
141
+ ```
142
+
143
+ ## Configuration
144
+
145
+ Configure via the `[tool.pythaw]` section in `pyproject.toml`.
146
+
147
+ ```toml
148
+ [tool.pythaw]
149
+ # Function name patterns recognized as handlers (fnmatch syntax)
150
+ handler_patterns = ["handler", "lambda_handler", "*_handler"]
151
+
152
+ # Patterns to exclude from scanning
153
+ exclude = [".venv", "tests"]
154
+
155
+ # Disable specific rules per file pattern
156
+ [tool.pythaw.per-file-ignores]
157
+ "tests/*" = ["PW001", "PW002"]
158
+ "scripts/*" = ["PW001"]
159
+ ```
160
+
161
+ ## License
162
+
163
+ [MIT](LICENSE)
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "pythaw"
3
+ version = "0.1.0"
4
+ description = "A Python static analysis tool that detects heavy initialization inside AWS Lambda handlers that should be moved to module scope for faster warm starts."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "MiuraToya" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ keywords = ["aws", "lambda", "static-analysis", "boto3", "linter"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "rich>=14.0.0",
28
+ "tomli>=2.0.0; python_version < '3.11'",
29
+ ]
30
+
31
+ [project.urls]
32
+ Repository = "https://github.com/MiuraToya/pythaw"
33
+
34
+ [project.scripts]
35
+ pythaw = "pythaw.cli:main"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "mypy>=1.19.1",
40
+ "pytest>=9.0.2",
41
+ "pytest-cov>=6.2.1",
42
+
43
+ "ruff>=0.15.4",
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.9.8,<0.11.0"]
48
+ build-backend = "uv_build"
49
+
50
+ [tool.uv.build-backend]
51
+ module-root = "."
52
+
53
+ [tool.mypy]
54
+ python_version = "3.10"
55
+ strict = true
56
+
57
+ [tool.ruff]
58
+ target-version = "py310"
59
+ extend-exclude = ["tests/e2e/scenarios", "tests/scenarios"]
60
+
61
+ [tool.ruff.lint]
62
+ select = ["ALL"]
63
+ ignore = [
64
+ "D", # pydocstyle — ドキュメント規約は別途決める
65
+ "ANN", # flake8-annotations — mypy strict でカバー
66
+ "COM812", # trailing comma — formatter と競合
67
+ "ISC001", # implicit string concat — formatter と競合
68
+ ]
69
+
70
+ [tool.ruff.lint.per-file-ignores]
71
+ "tests/*" = ["S101", "PLR2004"] # assert・マジックナンバーの使用を許可
72
+ "pythaw/cli.py" = ["T201", "TC003"] # CLI は print で出力し、Path を実行時に使用する
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from pythaw.cli import main
4
+
5
+ main()
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import os
5
+ import re
6
+ from fnmatch import fnmatch
7
+ from typing import TYPE_CHECKING, TypeAlias
8
+
9
+ from pythaw.finder import find_files
10
+ from pythaw.resolver import Resolver
11
+ from pythaw.rules import get_all_rules
12
+ from pythaw.violation import CallSite, Violation
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ from pythaw.config import Config
18
+ from pythaw.rules._base import Rule
19
+
20
+ FunctionNode: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
21
+
22
+
23
+ def check(
24
+ path: Path,
25
+ config: Config,
26
+ *,
27
+ select: frozenset[str] = frozenset(),
28
+ ignore: frozenset[str] = frozenset(),
29
+ ) -> list[Violation]:
30
+ """Run all rules against handler functions found under *path*.
31
+
32
+ Args:
33
+ path: File or directory to check.
34
+ config: Project configuration (handler patterns, excludes, etc.).
35
+ select: If non-empty, only run rules whose codes are in this set.
36
+ ignore: Rule codes to skip.
37
+
38
+ Returns:
39
+ A list of violations found across all handler functions.
40
+ """
41
+ files = find_files(path, config)
42
+ rules = _filter_rules(get_all_rules(), select=select, ignore=ignore)
43
+ violations: list[Violation] = []
44
+
45
+ base = path if path.is_dir() else path.parent
46
+ resolver = Resolver(base)
47
+ for file in files:
48
+ source = _read_source(file)
49
+ if source is None:
50
+ continue
51
+ if _has_nocheck(source):
52
+ continue
53
+ tree = _parse_source(source, file)
54
+ if tree is None:
55
+ continue
56
+ file_rules = _apply_per_file_ignores(rules, file, base, config.per_file_ignores)
57
+ suppressed = _parse_nopw_comments(source)
58
+ for func_node in _extract_handlers(tree, config.handler_patterns):
59
+ violations.extend(
60
+ _check_function(
61
+ file,
62
+ func_node,
63
+ file_rules,
64
+ suppressed,
65
+ resolver,
66
+ )
67
+ )
68
+
69
+ return violations
70
+
71
+
72
+ def _apply_per_file_ignores(
73
+ rules: tuple[Rule, ...],
74
+ file: Path,
75
+ base: Path,
76
+ per_file_ignores: tuple[tuple[str, tuple[str, ...]], ...],
77
+ ) -> tuple[Rule, ...]:
78
+ """Remove rules that match per-file-ignores patterns for *file*."""
79
+ if not per_file_ignores:
80
+ return rules
81
+ ignored_codes: set[str] = set()
82
+ try:
83
+ rel = str(file.resolve().relative_to(base.resolve()))
84
+ except ValueError:
85
+ rel = os.path.relpath(file)
86
+ for pattern, codes in per_file_ignores:
87
+ if fnmatch(rel, pattern):
88
+ ignored_codes.update(codes)
89
+ if not ignored_codes:
90
+ return rules
91
+ return tuple(r for r in rules if r.code not in ignored_codes)
92
+
93
+
94
+ def _filter_rules(
95
+ rules: tuple[Rule, ...],
96
+ *,
97
+ select: frozenset[str],
98
+ ignore: frozenset[str],
99
+ ) -> tuple[Rule, ...]:
100
+ """Filter rules by *select* and *ignore* code sets."""
101
+ filtered = rules
102
+ if select:
103
+ filtered = tuple(r for r in filtered if r.code in select)
104
+ if ignore:
105
+ filtered = tuple(r for r in filtered if r.code not in ignore)
106
+ return filtered
107
+
108
+
109
+ _NOPW_RE = re.compile(r"#\s*nopw:\s*(PW\d+(?:\s*,\s*PW\d+)*)")
110
+ _NOCHECK_RE = re.compile(r"^\s*#\s*pythaw:\s*nocheck\b")
111
+
112
+
113
+ def _read_source(file: Path) -> str | None:
114
+ """Read *file* and return its contents, or ``None`` on failure."""
115
+ try:
116
+ return file.read_text(encoding="utf-8")
117
+ except (UnicodeDecodeError, OSError):
118
+ return None
119
+
120
+
121
+ def _parse_source(source: str, file: Path) -> ast.Module | None:
122
+ """Parse *source* and return the AST, or ``None`` on failure."""
123
+ try:
124
+ return ast.parse(source, filename=str(file))
125
+ except SyntaxError:
126
+ return None
127
+
128
+
129
+ def _has_nocheck(source: str) -> bool:
130
+ """Return ``True`` if *source* contains a ``# pythaw: nocheck`` directive."""
131
+ for line in source.splitlines():
132
+ stripped = line.strip()
133
+ if not stripped or stripped.startswith("#"):
134
+ if _NOCHECK_RE.match(stripped):
135
+ return True
136
+ continue
137
+ break
138
+ return False
139
+
140
+
141
+ def _parse_nopw_comments(source: str) -> dict[int, frozenset[str]]:
142
+ """Extract per-line ``# nopw: PWXXX`` suppression directives.
143
+
144
+ Returns a mapping of line number to the set of suppressed rule codes.
145
+ """
146
+ suppressed: dict[int, frozenset[str]] = {}
147
+ for lineno, line in enumerate(source.splitlines(), start=1):
148
+ m = _NOPW_RE.search(line)
149
+ if m:
150
+ codes = frozenset(c.strip() for c in m.group(1).split(","))
151
+ suppressed[lineno] = codes
152
+ return suppressed
153
+
154
+
155
+ def _extract_handlers(
156
+ tree: ast.Module,
157
+ patterns: tuple[str, ...],
158
+ ) -> list[FunctionNode]:
159
+ """Return top-level function nodes whose name matches *patterns*."""
160
+ # Only inspect top-level nodes (iter_child_nodes does not recurse)
161
+ # so that nested functions and class methods are excluded.
162
+ return [
163
+ node
164
+ for node in ast.iter_child_nodes(tree)
165
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
166
+ and any(fnmatch(node.name, p) for p in patterns)
167
+ ]
168
+
169
+
170
+ def _check_function( # noqa: PLR0913
171
+ file: Path,
172
+ func_node: FunctionNode,
173
+ rules: tuple[Rule, ...],
174
+ suppressed: dict[int, frozenset[str]],
175
+ resolver: Resolver,
176
+ *,
177
+ chain: tuple[CallSite, ...] = (),
178
+ visited: set[tuple[str, str]] | None = None,
179
+ ) -> list[Violation]:
180
+ """Walk *func_node* and return violations, following local calls."""
181
+ if visited is None:
182
+ visited = set()
183
+
184
+ violations: list[Violation] = []
185
+ for node in ast.walk(func_node):
186
+ if not isinstance(node, ast.Call):
187
+ continue
188
+
189
+ # Check rule violations
190
+ suppressed_codes = suppressed.get(node.lineno, frozenset())
191
+ violations.extend(
192
+ Violation(
193
+ file=os.path.relpath(file),
194
+ line=node.lineno,
195
+ col=node.col_offset,
196
+ code=rule.code,
197
+ message=rule.message,
198
+ call_chain=chain,
199
+ )
200
+ for rule in rules
201
+ if rule.check(node) and rule.code not in suppressed_codes
202
+ )
203
+
204
+ # Follow resolved local calls
205
+ _follow_call(
206
+ file,
207
+ node,
208
+ rules,
209
+ resolver,
210
+ chain,
211
+ visited,
212
+ violations,
213
+ )
214
+
215
+ return violations
216
+
217
+
218
+ def _follow_call( # noqa: PLR0913
219
+ file: Path,
220
+ node: ast.Call,
221
+ rules: tuple[Rule, ...],
222
+ resolver: Resolver,
223
+ chain: tuple[CallSite, ...],
224
+ visited: set[tuple[str, str]],
225
+ violations: list[Violation],
226
+ ) -> None:
227
+ """Resolve *node* and recursively check the target definition."""
228
+ target = resolver.resolve_call(file, node)
229
+ if target is None:
230
+ return
231
+ target_file, target_defn = target
232
+
233
+ walkable: FunctionNode | None
234
+ if isinstance(target_defn, ast.ClassDef):
235
+ walkable = resolver.get_init(target_defn)
236
+ else:
237
+ walkable = target_defn
238
+ if walkable is None:
239
+ return
240
+
241
+ key = (str(target_file.resolve()), target_defn.name)
242
+ if key in visited:
243
+ return
244
+ visited.add(key)
245
+
246
+ site = CallSite(
247
+ file=os.path.relpath(file),
248
+ line=node.lineno,
249
+ col=node.col_offset,
250
+ name=resolver.call_display_name(node),
251
+ )
252
+ target_source = resolver.read_source(target_file)
253
+ target_suppressed = _parse_nopw_comments(target_source) if target_source else {}
254
+ violations.extend(
255
+ _check_function(
256
+ target_file,
257
+ walkable,
258
+ rules,
259
+ target_suppressed,
260
+ resolver,
261
+ chain=(*chain, site),
262
+ visited=visited,
263
+ )
264
+ )