sarj-python-lint 0.68.0__tar.gz → 0.69.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.
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-0.69.0/src/sarj_python_lint/rules/iac_source_coupled_test.py +107 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/source_coupled_test.py +193 -53
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/.gitignore +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/LICENSE +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/README.md +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.69.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -19,6 +19,7 @@ from sarj_python_lint.rules.duplicated_override_docstring import (
|
|
|
19
19
|
)
|
|
20
20
|
from sarj_python_lint.rules.fastapi_openapi_contract import FastapiOpenapiContract
|
|
21
21
|
from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
|
|
22
|
+
from sarj_python_lint.rules.iac_source_coupled_test import IacSourceCoupledTest
|
|
22
23
|
from sarj_python_lint.rules.invalid_pydantic_field_default import (
|
|
23
24
|
InvalidPydanticFieldDefault,
|
|
24
25
|
)
|
|
@@ -157,6 +158,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
157
158
|
NoCommentCruft.id: NoCommentCruft,
|
|
158
159
|
StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
|
|
159
160
|
SourceCoupledTest.id: SourceCoupledTest,
|
|
161
|
+
IacSourceCoupledTest.id: IacSourceCoupledTest,
|
|
160
162
|
NoQueryWithManyJoins.id: NoQueryWithManyJoins,
|
|
161
163
|
NoAggregationInStoreQuery.id: NoAggregationInStoreQuery,
|
|
162
164
|
NoSelectStar.id: NoSelectStar,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""SARJ412 — tests must not assert on raw IaC source text.
|
|
2
|
+
|
|
3
|
+
Examples: https://github.com/sarj-ai/standards/blob/main/packages/python/tests/rules/test_iac_source_coupled_test.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from pathlib import PurePosixPath
|
|
10
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
11
|
+
|
|
12
|
+
from sarj_python_lint.rule_base import (
|
|
13
|
+
Diagnostic,
|
|
14
|
+
ExampleFile,
|
|
15
|
+
ExampleOutcome,
|
|
16
|
+
Rule,
|
|
17
|
+
RuleCategory,
|
|
18
|
+
RuleDocumentation,
|
|
19
|
+
RuleExample,
|
|
20
|
+
Severity,
|
|
21
|
+
parse_or_none,
|
|
22
|
+
)
|
|
23
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
24
|
+
from sarj_python_lint.rules.source_coupled_test import FunctionAnalyzer, top_level_test_functions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
IAC_SOURCE_SUFFIXES = (".hcl", ".tf", ".tf.json", ".tfvars", ".tftest.hcl", ".tftest.json")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@final
|
|
35
|
+
class IacSourceCoupledTest(Rule):
|
|
36
|
+
id = "iac-source-coupled-test"
|
|
37
|
+
code = "SARJ412"
|
|
38
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
39
|
+
summary="Test asserts on raw IaC source text instead of a parsed plan, provider state, or runtime behavior.",
|
|
40
|
+
rationale=(
|
|
41
|
+
"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently."
|
|
42
|
+
),
|
|
43
|
+
remediation="Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
44
|
+
category=RuleCategory.TESTING,
|
|
45
|
+
limitations=(
|
|
46
|
+
"The rule follows local aliases, path collections, context-managed reads, and common normalization; interprocedural flows remain unreported.",
|
|
47
|
+
"Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
|
|
48
|
+
"The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible.",
|
|
49
|
+
),
|
|
50
|
+
examples=(
|
|
51
|
+
RuleExample(
|
|
52
|
+
example_id="rendered-plan-contract",
|
|
53
|
+
title="Assert on rendered Terraform plan behavior",
|
|
54
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
55
|
+
files=(
|
|
56
|
+
ExampleFile.python(
|
|
57
|
+
"tests/test_policy.py",
|
|
58
|
+
"def test_policy():\n plan = json.loads(Path('plan.json').read_text())\n assert verify(plan) == []\n",
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
62
|
+
expected_count=0,
|
|
63
|
+
public=True,
|
|
64
|
+
),
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="terraform-substring-contract",
|
|
67
|
+
title="Do not prove Terraform behavior with a substring",
|
|
68
|
+
outcome=ExampleOutcome.MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"tests/test_policy.py",
|
|
72
|
+
"def test_policy():\n source = Path('main.tf').read_text()\n assert 'prevent_destroy = true' in source\n",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
76
|
+
expected_count=1,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
description = documentation.summary
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
85
|
+
if not is_test_path(path) or is_generated(path, source):
|
|
86
|
+
return []
|
|
87
|
+
tree = parse_or_none(path, source)
|
|
88
|
+
if not isinstance(tree, ast.Module):
|
|
89
|
+
return []
|
|
90
|
+
assertions = [
|
|
91
|
+
assertion
|
|
92
|
+
for function, unittest_style in top_level_test_functions(tree)
|
|
93
|
+
for assertion in FunctionAnalyzer(IAC_SOURCE_SUFFIXES, unittest_style=unittest_style).analyze(function)
|
|
94
|
+
]
|
|
95
|
+
return [
|
|
96
|
+
Diagnostic(
|
|
97
|
+
path=path,
|
|
98
|
+
line=assertion.lineno,
|
|
99
|
+
col=assertion.col_offset + 1,
|
|
100
|
+
code=self.code,
|
|
101
|
+
severity=Severity.WARNING,
|
|
102
|
+
message=(
|
|
103
|
+
"raw IaC source text is the test oracle; inspect rendered plan JSON, provider state, or runtime behavior instead."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
for assertion in assertions
|
|
107
|
+
]
|
|
@@ -27,15 +27,12 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
GENERAL_SOURCE_SUFFIXES = (
|
|
31
31
|
".bash",
|
|
32
|
-
".hcl",
|
|
33
32
|
".js",
|
|
34
33
|
".mjs",
|
|
35
34
|
".py",
|
|
36
35
|
".sh",
|
|
37
|
-
".tf",
|
|
38
|
-
".tfvars",
|
|
39
36
|
".ts",
|
|
40
37
|
".yaml",
|
|
41
38
|
".yml",
|
|
@@ -46,6 +43,26 @@ _TEXT_TRANSFORMS = frozenset(
|
|
|
46
43
|
_TEXT_ASSERTIONS = frozenset({"count", "endswith", "find", "index", "startswith"})
|
|
47
44
|
_REGEX_ASSERTIONS = frozenset({"findall", "finditer", "fullmatch", "match", "search"})
|
|
48
45
|
_TEMP_PATH_NAMES = frozenset({"tmp_path", "tmpdir", "temp_dir", "temporary_directory"})
|
|
46
|
+
_UNITTEST_ASSERTIONS = frozenset(
|
|
47
|
+
{
|
|
48
|
+
"assertEqual",
|
|
49
|
+
"assertFalse",
|
|
50
|
+
"assertGreater",
|
|
51
|
+
"assertGreaterEqual",
|
|
52
|
+
"assertIn",
|
|
53
|
+
"assertIs",
|
|
54
|
+
"assertIsNone",
|
|
55
|
+
"assertIsNot",
|
|
56
|
+
"assertIsNotNone",
|
|
57
|
+
"assertNotEqual",
|
|
58
|
+
"assertNotIn",
|
|
59
|
+
"assertNotRegex",
|
|
60
|
+
"assertRegex",
|
|
61
|
+
"assertLess",
|
|
62
|
+
"assertLessEqual",
|
|
63
|
+
"assertTrue",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
49
66
|
|
|
50
67
|
|
|
51
68
|
@final
|
|
@@ -55,7 +72,7 @@ class SourceCoupledTest(Rule):
|
|
|
55
72
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
56
73
|
summary="Test asserts on raw repository source text instead of parsed or executable behavior.",
|
|
57
74
|
rationale="Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
|
|
58
|
-
remediation="Parse the artifact, execute its validator, or assert on
|
|
75
|
+
remediation="Parse the artifact, execute its validator, or assert on a runtime contract.",
|
|
59
76
|
category=RuleCategory.TESTING,
|
|
60
77
|
limitations=(
|
|
61
78
|
"The rule follows local aliases, path aliases, context-managed reads, and common text normalization; interprocedural flows remain unreported.",
|
|
@@ -64,13 +81,13 @@ class SourceCoupledTest(Rule):
|
|
|
64
81
|
),
|
|
65
82
|
examples=(
|
|
66
83
|
RuleExample(
|
|
67
|
-
example_id="parsed-
|
|
68
|
-
title="Assert on parsed
|
|
84
|
+
example_id="parsed-workflow-contract",
|
|
85
|
+
title="Assert on parsed workflow behavior",
|
|
69
86
|
outcome=ExampleOutcome.NO_MATCH,
|
|
70
87
|
files=(
|
|
71
88
|
ExampleFile.python(
|
|
72
89
|
"tests/test_policy.py",
|
|
73
|
-
"def test_policy():\n
|
|
90
|
+
"def test_policy():\n workflow = yaml.safe_load(Path('workflow.yml').read_text())\n assert verify(workflow) == []\n",
|
|
74
91
|
),
|
|
75
92
|
),
|
|
76
93
|
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
@@ -78,13 +95,13 @@ class SourceCoupledTest(Rule):
|
|
|
78
95
|
public=True,
|
|
79
96
|
),
|
|
80
97
|
RuleExample(
|
|
81
|
-
example_id="
|
|
82
|
-
title="Do not prove
|
|
98
|
+
example_id="workflow-substring-contract",
|
|
99
|
+
title="Do not prove workflow behavior with a substring",
|
|
83
100
|
outcome=ExampleOutcome.MATCH,
|
|
84
101
|
files=(
|
|
85
102
|
ExampleFile.python(
|
|
86
103
|
"tests/test_policy.py",
|
|
87
|
-
"def test_policy():\n source = Path('
|
|
104
|
+
"def test_policy():\n source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n",
|
|
88
105
|
),
|
|
89
106
|
),
|
|
90
107
|
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
@@ -104,8 +121,8 @@ class SourceCoupledTest(Rule):
|
|
|
104
121
|
return []
|
|
105
122
|
assertions = [
|
|
106
123
|
assertion
|
|
107
|
-
for function in
|
|
108
|
-
for assertion in
|
|
124
|
+
for function, unittest_style in top_level_test_functions(tree)
|
|
125
|
+
for assertion in FunctionAnalyzer(GENERAL_SOURCE_SUFFIXES, unittest_style=unittest_style).analyze(function)
|
|
109
126
|
]
|
|
110
127
|
return [
|
|
111
128
|
Diagnostic(
|
|
@@ -122,33 +139,43 @@ class SourceCoupledTest(Rule):
|
|
|
122
139
|
]
|
|
123
140
|
|
|
124
141
|
|
|
125
|
-
def
|
|
126
|
-
functions: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
|
|
142
|
+
def top_level_test_functions(tree: ast.Module) -> list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, bool]]:
|
|
143
|
+
functions: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, bool]] = []
|
|
127
144
|
for statement in tree.body:
|
|
128
145
|
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)) and statement.name.startswith("test"):
|
|
129
|
-
functions.append(statement)
|
|
146
|
+
functions.append((statement, False))
|
|
130
147
|
elif isinstance(statement, ast.ClassDef):
|
|
148
|
+
unittest_style = any(
|
|
149
|
+
(isinstance(base, ast.Name) and base.id == "TestCase")
|
|
150
|
+
or (isinstance(base, ast.Attribute) and base.attr == "TestCase")
|
|
151
|
+
for base in statement.bases
|
|
152
|
+
)
|
|
131
153
|
functions.extend(
|
|
132
|
-
child
|
|
154
|
+
(child, unittest_style)
|
|
133
155
|
for child in statement.body
|
|
134
156
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)) and child.name.startswith("test")
|
|
135
157
|
)
|
|
136
158
|
return functions
|
|
137
159
|
|
|
138
160
|
|
|
139
|
-
class
|
|
161
|
+
class FunctionAnalyzer(ast.NodeVisitor):
|
|
140
162
|
"""Track raw text inside one test without leaking into nested lexical scopes."""
|
|
141
163
|
|
|
142
|
-
|
|
164
|
+
_source_suffixes: tuple[str, ...]
|
|
165
|
+
_unittest_style: bool
|
|
166
|
+
|
|
167
|
+
def __init__(self, source_suffixes: tuple[str, ...], *, unittest_style: bool = False) -> None:
|
|
168
|
+
self._source_suffixes = source_suffixes
|
|
169
|
+
self._unittest_style = unittest_style
|
|
143
170
|
self._collections: set[str] = set()
|
|
144
171
|
self._ephemeral_paths: set[str] = set()
|
|
145
172
|
self._paths: set[str] = set()
|
|
146
173
|
self._raw: set[str] = set()
|
|
147
174
|
self._raw_origins: dict[str, set[int]] = {}
|
|
148
175
|
self._reported_origins: set[int] = set()
|
|
149
|
-
self._assertions: list[ast.Assert] = []
|
|
176
|
+
self._assertions: list[ast.Assert | ast.Call] = []
|
|
150
177
|
|
|
151
|
-
def analyze(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.Assert]:
|
|
178
|
+
def analyze(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.Assert | ast.Call]:
|
|
152
179
|
arguments = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
|
|
153
180
|
self._ephemeral_paths.update(argument.arg for argument in arguments if argument.arg in _TEMP_PATH_NAMES)
|
|
154
181
|
for statement in function.body:
|
|
@@ -194,7 +221,7 @@ class _FunctionAnalyzer(ast.NodeVisitor):
|
|
|
194
221
|
for item in node.items:
|
|
195
222
|
self.visit(item.context_expr)
|
|
196
223
|
if isinstance(item.optional_vars, ast.Name) and _is_source_open(
|
|
197
|
-
item.context_expr, self._paths, self._ephemeral_paths
|
|
224
|
+
item.context_expr, self._paths, self._ephemeral_paths, self._source_suffixes
|
|
198
225
|
):
|
|
199
226
|
self._raw.add(item.optional_vars.id)
|
|
200
227
|
self._raw_origins[item.optional_vars.id] = {item.context_expr.lineno}
|
|
@@ -207,10 +234,23 @@ class _FunctionAnalyzer(ast.NodeVisitor):
|
|
|
207
234
|
|
|
208
235
|
@override
|
|
209
236
|
def visit_Assert(self, node: ast.Assert) -> None:
|
|
210
|
-
if _raw_text_oracle(node.test, self._raw, self._paths, self._ephemeral_paths):
|
|
211
|
-
origins = _expression_origins(
|
|
212
|
-
node.
|
|
213
|
-
}
|
|
237
|
+
if _raw_text_oracle(node.test, self._raw, self._paths, self._ephemeral_paths, self._source_suffixes):
|
|
238
|
+
origins = _expression_origins(
|
|
239
|
+
node.test, self._raw_origins, self._paths, self._ephemeral_paths, self._source_suffixes
|
|
240
|
+
) or {node.lineno}
|
|
241
|
+
if not origins.issubset(self._reported_origins):
|
|
242
|
+
self._assertions.append(node)
|
|
243
|
+
self._reported_origins.update(origins)
|
|
244
|
+
self.generic_visit(node)
|
|
245
|
+
|
|
246
|
+
@override
|
|
247
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
248
|
+
if self._unittest_style and _unittest_raw_text_oracle(
|
|
249
|
+
node, self._raw, self._paths, self._ephemeral_paths, self._source_suffixes
|
|
250
|
+
):
|
|
251
|
+
origins = _expression_origins(
|
|
252
|
+
node, self._raw_origins, self._paths, self._ephemeral_paths, self._source_suffixes
|
|
253
|
+
) or {node.lineno}
|
|
214
254
|
if not origins.issubset(self._reported_origins):
|
|
215
255
|
self._assertions.append(node)
|
|
216
256
|
self._reported_origins.update(origins)
|
|
@@ -234,7 +274,9 @@ class _FunctionAnalyzer(ast.NodeVisitor):
|
|
|
234
274
|
def _record_target(self, target: ast.expr, value: ast.expr) -> None:
|
|
235
275
|
if not isinstance(target, ast.Name):
|
|
236
276
|
return
|
|
237
|
-
raw_origins = _expression_origins(
|
|
277
|
+
raw_origins = _expression_origins(
|
|
278
|
+
value, self._raw_origins, self._paths, self._ephemeral_paths, self._source_suffixes
|
|
279
|
+
)
|
|
238
280
|
# Reassignment kills stale taint before adding the new value's state.
|
|
239
281
|
self._paths.discard(target.id)
|
|
240
282
|
self._raw.discard(target.id)
|
|
@@ -243,36 +285,43 @@ class _FunctionAnalyzer(ast.NodeVisitor):
|
|
|
243
285
|
self._ephemeral_paths.discard(target.id)
|
|
244
286
|
if _ephemeral_path_expression(value, self._ephemeral_paths):
|
|
245
287
|
self._ephemeral_paths.add(target.id)
|
|
246
|
-
if _source_path_collection(value, self._paths):
|
|
288
|
+
if _source_path_collection(value, self._paths, self._source_suffixes):
|
|
247
289
|
self._collections.add(target.id)
|
|
248
|
-
if _source_path_expression(value, self._paths):
|
|
290
|
+
if _source_path_expression(value, self._paths, self._source_suffixes):
|
|
249
291
|
self._paths.add(target.id)
|
|
250
|
-
if _raw_text_expression(value, self._raw, self._paths, self._ephemeral_paths):
|
|
292
|
+
if _raw_text_expression(value, self._raw, self._paths, self._ephemeral_paths, self._source_suffixes):
|
|
251
293
|
self._raw.add(target.id)
|
|
252
294
|
self._raw_origins[target.id] = raw_origins or {value.lineno}
|
|
253
295
|
|
|
254
296
|
|
|
255
|
-
def _is_source_open(
|
|
297
|
+
def _is_source_open(
|
|
298
|
+
node: ast.expr,
|
|
299
|
+
path_names: set[str],
|
|
300
|
+
ephemeral_path_names: set[str],
|
|
301
|
+
source_suffixes: tuple[str, ...],
|
|
302
|
+
) -> bool:
|
|
256
303
|
return (
|
|
257
304
|
isinstance(node, ast.Call)
|
|
258
305
|
and isinstance(node.func, ast.Name)
|
|
259
306
|
and node.func.id == "open"
|
|
260
307
|
and bool(node.args)
|
|
261
|
-
and _source_path_expression(node.args[0], path_names)
|
|
308
|
+
and _source_path_expression(node.args[0], path_names, source_suffixes)
|
|
262
309
|
and not _ephemeral_path_expression(node.args[0], ephemeral_path_names)
|
|
263
310
|
)
|
|
264
311
|
|
|
265
312
|
|
|
266
|
-
def _source_path_expression(node: ast.AST, path_names: set[str]) -> bool:
|
|
313
|
+
def _source_path_expression(node: ast.AST, path_names: set[str], source_suffixes: tuple[str, ...]) -> bool:
|
|
267
314
|
match node:
|
|
268
315
|
case ast.Name(id=name):
|
|
269
316
|
return name in path_names
|
|
270
317
|
case ast.Constant(value=str(value)):
|
|
271
|
-
return value.lower().endswith(
|
|
318
|
+
return value.lower().endswith(source_suffixes)
|
|
272
319
|
case ast.JoinedStr(values=values):
|
|
273
|
-
return any(_source_path_expression(value, path_names) for value in values)
|
|
320
|
+
return any(_source_path_expression(value, path_names, source_suffixes) for value in values)
|
|
274
321
|
case ast.BinOp() | ast.Call() | ast.Attribute() | ast.Subscript():
|
|
275
|
-
return any(
|
|
322
|
+
return any(
|
|
323
|
+
_source_path_expression(child, path_names, source_suffixes) for child in ast.iter_child_nodes(node)
|
|
324
|
+
)
|
|
276
325
|
case _:
|
|
277
326
|
return False
|
|
278
327
|
|
|
@@ -281,77 +330,168 @@ def _ephemeral_path_expression(node: ast.AST, ephemeral_path_names: set[str]) ->
|
|
|
281
330
|
return any(isinstance(child, ast.Name) and child.id in ephemeral_path_names for child in ast.walk(node))
|
|
282
331
|
|
|
283
332
|
|
|
284
|
-
def _source_path_collection(node: ast.AST, path_names: set[str]) -> bool:
|
|
333
|
+
def _source_path_collection(node: ast.AST, path_names: set[str], source_suffixes: tuple[str, ...]) -> bool:
|
|
285
334
|
return (
|
|
286
335
|
isinstance(node, (ast.List, ast.Set, ast.Tuple))
|
|
287
336
|
and bool(node.elts)
|
|
288
|
-
and all(_source_path_expression(element, path_names) for element in node.elts)
|
|
337
|
+
and all(_source_path_expression(element, path_names, source_suffixes) for element in node.elts)
|
|
289
338
|
)
|
|
290
339
|
|
|
291
340
|
|
|
292
|
-
def _raw_source_read(
|
|
341
|
+
def _raw_source_read(
|
|
342
|
+
node: ast.expr,
|
|
343
|
+
path_names: set[str],
|
|
344
|
+
ephemeral_path_names: set[str],
|
|
345
|
+
source_suffixes: tuple[str, ...],
|
|
346
|
+
) -> bool:
|
|
293
347
|
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
294
348
|
return False
|
|
295
349
|
if node.func.attr == "read_text":
|
|
296
|
-
return _source_path_expression(node.func.value, path_names) and not _ephemeral_path_expression(
|
|
350
|
+
return _source_path_expression(node.func.value, path_names, source_suffixes) and not _ephemeral_path_expression(
|
|
297
351
|
node.func.value, ephemeral_path_names
|
|
298
352
|
)
|
|
299
|
-
return node.func.attr == "read" and _is_source_open(
|
|
353
|
+
return node.func.attr == "read" and _is_source_open(
|
|
354
|
+
node.func.value, path_names, ephemeral_path_names, source_suffixes
|
|
355
|
+
)
|
|
300
356
|
|
|
301
357
|
|
|
302
358
|
def _raw_text_expression(
|
|
303
|
-
node: ast.expr,
|
|
359
|
+
node: ast.expr,
|
|
360
|
+
raw_names: set[str],
|
|
361
|
+
path_names: set[str],
|
|
362
|
+
ephemeral_path_names: set[str],
|
|
363
|
+
source_suffixes: tuple[str, ...],
|
|
304
364
|
) -> bool:
|
|
305
365
|
if isinstance(node, ast.Name):
|
|
306
366
|
return node.id in raw_names
|
|
307
|
-
if _raw_source_read(node, path_names, ephemeral_path_names):
|
|
367
|
+
if _raw_source_read(node, path_names, ephemeral_path_names, source_suffixes):
|
|
308
368
|
return True
|
|
309
369
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
310
370
|
if node.func.attr == "read" and isinstance(node.func.value, ast.Name):
|
|
311
371
|
return node.func.value.id in raw_names
|
|
312
372
|
return node.func.attr in _TEXT_TRANSFORMS and _raw_text_expression(
|
|
313
|
-
node.func.value, raw_names, path_names, ephemeral_path_names
|
|
373
|
+
node.func.value, raw_names, path_names, ephemeral_path_names, source_suffixes
|
|
314
374
|
)
|
|
315
375
|
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
316
|
-
return _raw_text_expression(
|
|
317
|
-
node.
|
|
318
|
-
)
|
|
376
|
+
return _raw_text_expression(
|
|
377
|
+
node.left, raw_names, path_names, ephemeral_path_names, source_suffixes
|
|
378
|
+
) or _raw_text_expression(node.right, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
319
379
|
return False
|
|
320
380
|
|
|
321
381
|
|
|
322
|
-
def _raw_text_oracle(
|
|
382
|
+
def _raw_text_oracle(
|
|
383
|
+
node: ast.expr,
|
|
384
|
+
raw_names: set[str],
|
|
385
|
+
path_names: set[str],
|
|
386
|
+
ephemeral_path_names: set[str],
|
|
387
|
+
source_suffixes: tuple[str, ...],
|
|
388
|
+
) -> bool:
|
|
323
389
|
if isinstance(node, ast.Compare):
|
|
324
390
|
operands = [node.left, *node.comparators]
|
|
325
391
|
if any(
|
|
326
|
-
|
|
392
|
+
_raw_text_measurement(operand, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
393
|
+
for operand in operands
|
|
394
|
+
) and any(isinstance(operator, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)) for operator in node.ops):
|
|
395
|
+
return True
|
|
396
|
+
if any(
|
|
397
|
+
_raw_text_expression(operand, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
398
|
+
for operand in operands
|
|
327
399
|
) and any(isinstance(operator, (ast.In, ast.NotIn, ast.Eq, ast.NotEq)) for operator in node.ops):
|
|
328
400
|
return True
|
|
401
|
+
if (
|
|
402
|
+
isinstance(node, ast.Call)
|
|
403
|
+
and isinstance(node.func, ast.Name)
|
|
404
|
+
and node.func.id in {"all", "any"}
|
|
405
|
+
and any(
|
|
406
|
+
isinstance(argument, ast.GeneratorExp)
|
|
407
|
+
and any(
|
|
408
|
+
_raw_text_line_iteration(item.iter, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
409
|
+
for item in argument.generators
|
|
410
|
+
)
|
|
411
|
+
for argument in node.args
|
|
412
|
+
)
|
|
413
|
+
):
|
|
414
|
+
return True
|
|
329
415
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
330
416
|
if node.func.attr in _TEXT_ASSERTIONS and _raw_text_expression(
|
|
331
|
-
node.func.value, raw_names, path_names, ephemeral_path_names
|
|
417
|
+
node.func.value, raw_names, path_names, ephemeral_path_names, source_suffixes
|
|
332
418
|
):
|
|
333
419
|
return True
|
|
334
420
|
if node.func.attr in _REGEX_ASSERTIONS and any(
|
|
335
|
-
_raw_text_expression(argument, raw_names, path_names, ephemeral_path_names)
|
|
421
|
+
_raw_text_expression(argument, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
422
|
+
for argument in node.args
|
|
336
423
|
):
|
|
337
424
|
return True
|
|
338
425
|
return any(
|
|
339
|
-
_raw_text_oracle(child, raw_names, path_names, ephemeral_path_names)
|
|
426
|
+
_raw_text_oracle(child, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
340
427
|
for child in ast.iter_child_nodes(node)
|
|
341
428
|
if isinstance(child, ast.expr)
|
|
342
429
|
)
|
|
343
430
|
|
|
344
431
|
|
|
432
|
+
def _raw_text_measurement(
|
|
433
|
+
node: ast.expr,
|
|
434
|
+
raw_names: set[str],
|
|
435
|
+
path_names: set[str],
|
|
436
|
+
ephemeral_path_names: set[str],
|
|
437
|
+
source_suffixes: tuple[str, ...],
|
|
438
|
+
) -> bool:
|
|
439
|
+
return (
|
|
440
|
+
isinstance(node, ast.Call)
|
|
441
|
+
and isinstance(node.func, ast.Name)
|
|
442
|
+
and node.func.id == "len"
|
|
443
|
+
and len(node.args) == 1
|
|
444
|
+
and _raw_text_expression(node.args[0], raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _raw_text_line_iteration(
|
|
449
|
+
node: ast.expr,
|
|
450
|
+
raw_names: set[str],
|
|
451
|
+
path_names: set[str],
|
|
452
|
+
ephemeral_path_names: set[str],
|
|
453
|
+
source_suffixes: tuple[str, ...],
|
|
454
|
+
) -> bool:
|
|
455
|
+
return (
|
|
456
|
+
isinstance(node, ast.Call)
|
|
457
|
+
and isinstance(node.func, ast.Attribute)
|
|
458
|
+
and node.func.attr == "splitlines"
|
|
459
|
+
and _raw_text_expression(node.func.value, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _unittest_raw_text_oracle(
|
|
464
|
+
node: ast.Call,
|
|
465
|
+
raw_names: set[str],
|
|
466
|
+
path_names: set[str],
|
|
467
|
+
ephemeral_path_names: set[str],
|
|
468
|
+
source_suffixes: tuple[str, ...],
|
|
469
|
+
) -> bool:
|
|
470
|
+
return (
|
|
471
|
+
isinstance(node.func, ast.Attribute)
|
|
472
|
+
and isinstance(node.func.value, ast.Name)
|
|
473
|
+
and node.func.value.id in {"self", "cls"}
|
|
474
|
+
and node.func.attr in _UNITTEST_ASSERTIONS
|
|
475
|
+
and any(
|
|
476
|
+
_raw_text_expression(argument, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
477
|
+
or _raw_text_measurement(argument, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
478
|
+
or _raw_text_oracle(argument, raw_names, path_names, ephemeral_path_names, source_suffixes)
|
|
479
|
+
for argument in node.args
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
345
484
|
def _expression_origins(
|
|
346
485
|
node: ast.AST,
|
|
347
486
|
raw_origins: dict[str, set[int]],
|
|
348
487
|
path_names: set[str],
|
|
349
488
|
ephemeral_path_names: set[str],
|
|
489
|
+
source_suffixes: tuple[str, ...],
|
|
350
490
|
) -> set[int]:
|
|
351
491
|
origins: set[int] = set()
|
|
352
492
|
for child in ast.walk(node):
|
|
353
493
|
if isinstance(child, ast.Name):
|
|
354
494
|
origins.update(raw_origins.get(child.id, set()))
|
|
355
|
-
elif isinstance(child, ast.Call) and _raw_source_read(child, path_names, ephemeral_path_names):
|
|
495
|
+
elif isinstance(child, ast.Call) and _raw_source_read(child, path_names, ephemeral_path_names, source_suffixes):
|
|
356
496
|
origins.add(child.lineno)
|
|
357
497
|
return origins
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_long_comment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/no_stdlib_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.68.0 → sarj_python_lint-0.69.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|