typedframes 0.6.0__tar.gz → 0.7.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.
- {typedframes-0.6.0 → typedframes-0.7.0}/PKG-INFO +2 -2
- {typedframes-0.6.0 → typedframes-0.7.0}/README.md +1 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/pyproject.toml +1 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/Cargo.lock +1 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/Cargo.toml +1 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/constants.rs +3 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/errors.rs +44 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/index.rs +315 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/linter.rs +1284 -129
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/notebook.rs +77 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/pyapi.rs +4 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/__init__.py +1 -1
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/cli.py +175 -23
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/.cargo/config.toml +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/README.md +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/benches/parser_bench.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/ast_extract.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/config.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/contract.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/lib.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/main.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/sql.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/src/typo.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/rust/tests/integration_test.rs +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/_rust_checker.pyi +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/base_schema.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/column.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/column_group.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/column_group_error.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/column_set.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/missing_dependency_error.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/mypy.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/pandas.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/pandera.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/polars.py +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/py.typed +0 -0
- {typedframes-0.6.0 → typedframes-0.7.0}/src/typedframes/schema_algebra.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typedframes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -556,7 +556,7 @@ The action installs the PyPI wheel into a throwaway virtualenv and runs the chec
|
|
|
556
556
|
| `version` | `latest` | PyPI version to install, e.g. `"0.5.0"` |
|
|
557
557
|
| `strict` | `true` | Fail the step on errors — `typedframes check` exits 0 without it |
|
|
558
558
|
| `coverage-fail-under` | *(unset)* | Minimum DataFrame schema coverage, e.g. `"90"` |
|
|
559
|
-
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown |
|
|
559
|
+
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown, `explain` to diagnose a lower-than-expected total |
|
|
560
560
|
| `no-warnings` | `false` | Suppress warning-level diagnostics |
|
|
561
561
|
| `args` | *(empty)* | Escape hatch for `--no-index`, `--lenient-ingest`, `--no-info` |
|
|
562
562
|
|
|
@@ -522,7 +522,7 @@ The action installs the PyPI wheel into a throwaway virtualenv and runs the chec
|
|
|
522
522
|
| `version` | `latest` | PyPI version to install, e.g. `"0.5.0"` |
|
|
523
523
|
| `strict` | `true` | Fail the step on errors — `typedframes check` exits 0 without it |
|
|
524
524
|
| `coverage-fail-under` | *(unset)* | Minimum DataFrame schema coverage, e.g. `"90"` |
|
|
525
|
-
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown |
|
|
525
|
+
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown, `explain` to diagnose a lower-than-expected total |
|
|
526
526
|
| `no-warnings` | `false` | Suppress warning-level diagnostics |
|
|
527
527
|
| `args` | *(empty)* | Escape hatch for `--no-index`, `--lenient-ingest`, `--no-info` |
|
|
528
528
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typedframes"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Static analysis for pandas and polars DataFrames. Catch column errors at lint-time, not runtime."
|
|
9
9
|
keywords = ["pandas", "polars", "type-checking", "static-analysis", "dataframe", "linter", "mypy-plugin"]
|
|
10
10
|
readme = "README.md"
|
|
@@ -182,6 +182,9 @@ pub(crate) const LOAD_FUNCTIONS: &[&str] = &[
|
|
|
182
182
|
"read_database",
|
|
183
183
|
"read_database_uri",
|
|
184
184
|
"read_gbq",
|
|
185
|
+
// Not an I/O read, but shares the same usecols=/columns=/dtype=/schema= recognition
|
|
186
|
+
// and bare-load-without-a-hint fallback as every function above it.
|
|
187
|
+
"DataFrame",
|
|
185
188
|
];
|
|
186
189
|
|
|
187
190
|
pub(crate) const LOAD_MODULES: &[&str] = &["pd", "pandas", "pl", "polars"];
|
|
@@ -63,6 +63,15 @@ pub struct FileStats {
|
|
|
63
63
|
pub dataframes_total: usize,
|
|
64
64
|
pub dataframes_typed: usize,
|
|
65
65
|
pub untyped_sites: Vec<UntypedSite>,
|
|
66
|
+
pub typed_sites: Vec<TypedSite>,
|
|
67
|
+
/// Every `<load module>.<load function>(...)`-shaped call found anywhere in the
|
|
68
|
+
/// file by an unconditional, position-independent scan (see
|
|
69
|
+
/// `linter::LoadCallSiteCollector`) -- populated regardless of whether the site
|
|
70
|
+
/// was actually recognized as an origin by the targeted (`Assign`/`AnnAssign`-only)
|
|
71
|
+
/// counting logic. Powers `--coverage-detail=explain`: cross-referencing this list
|
|
72
|
+
/// against `typed_sites`/`untyped_sites` by line is what tells a reader whether a
|
|
73
|
+
/// given call was counted at all, not just whether it was typed.
|
|
74
|
+
pub all_dataframe_calls: Vec<DataFrameCallSite>,
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/// One DataFrame origin the linter recognized but could not resolve columns for.
|
|
@@ -85,6 +94,41 @@ pub struct UntypedSite {
|
|
|
85
94
|
pub var: String,
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/// A recognized DataFrame origin that DID resolve to a concrete column set --
|
|
98
|
+
/// the `dataframes_typed` counterpart of [`UntypedSite`]. Line and column are
|
|
99
|
+
/// 1-indexed, matching [`LintError`].
|
|
100
|
+
#[derive(Debug, Serialize)]
|
|
101
|
+
pub struct TypedSite {
|
|
102
|
+
pub line: usize,
|
|
103
|
+
pub col: usize,
|
|
104
|
+
/// The assigned variable name where one was available, else a generic stand-in.
|
|
105
|
+
pub var: String,
|
|
106
|
+
/// A human-readable rendering of the resolved schema, from
|
|
107
|
+
/// `Linter::schema_display`: the schema's class name and column set for a named
|
|
108
|
+
/// `BaseSchema`, or `inferred column set {...}` (plus an origin, where known)
|
|
109
|
+
/// for one inferred from `usecols=`/a dict literal/etc.
|
|
110
|
+
pub schema: String,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// One `<load module>.<load function>(...)`-shaped call site found anywhere in a
|
|
114
|
+
/// file by [`crate::linter::LoadCallSiteCollector`]'s unconditional scan -- see
|
|
115
|
+
/// [`FileStats::all_dataframe_calls`]. Line and column are 1-indexed and point at
|
|
116
|
+
/// the call expression itself (e.g. the `pd` in `pd.DataFrame(...)`), NOT at an
|
|
117
|
+
/// enclosing assignment statement the way [`UntypedSite`]/[`TypedSite`] do -- the two
|
|
118
|
+
/// coordinate systems are cross-referenced by LINE only (see
|
|
119
|
+
/// `--coverage-detail=explain` in the CLI), not by column.
|
|
120
|
+
#[derive(Debug, Serialize)]
|
|
121
|
+
pub struct DataFrameCallSite {
|
|
122
|
+
pub line: usize,
|
|
123
|
+
pub col: usize,
|
|
124
|
+
/// E.g. `"pd.DataFrame"`, `"pl.read_csv"`.
|
|
125
|
+
pub label: String,
|
|
126
|
+
/// Human-readable description of the syntactic position this call was found in
|
|
127
|
+
/// (direct assignment, return value, call argument, collection element, ...) --
|
|
128
|
+
/// see `linter::CallContext::describe`.
|
|
129
|
+
pub context: String,
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
/// The JSON payload returned by the `check_file` entry point: the diagnostics plus
|
|
89
133
|
/// coverage stats.
|
|
90
134
|
#[derive(Debug, Serialize)]
|
|
@@ -9,7 +9,7 @@ use crate::ast_extract;
|
|
|
9
9
|
use crate::config::{find_project_root_opt, load_linter_config, LinterConfig};
|
|
10
10
|
use crate::constants::DEFAULT_EXCLUDED_DIRS;
|
|
11
11
|
use crate::errors::{LintError, CODE_UNKNOWN_COLUMN, CODE_UNTRACKED_DATAFRAME};
|
|
12
|
-
use crate::linter::{Linter, ParamGovernedTemplate};
|
|
12
|
+
use crate::linter::{collect_self_attr_origins, Linter, ParamGovernedTemplate, SelfAttrOrigin};
|
|
13
13
|
use ruff_python_ast::{self as ast, Expr, Stmt};
|
|
14
14
|
use ruff_python_parser::parse_module;
|
|
15
15
|
use ruff_source_file::{LineIndex, SourceCode};
|
|
@@ -139,6 +139,33 @@ pub(crate) struct ProjectIndex {
|
|
|
139
139
|
// warning and it would be wrong to silently report nothing).
|
|
140
140
|
#[serde(default)]
|
|
141
141
|
pub(crate) resolved_governed: std::collections::HashSet<(String, String)>,
|
|
142
|
+
// Defining file -> bare function/method names confirmed called from a direct
|
|
143
|
+
// `x = name(...)` call site SOMEWHERE in the project (any file, including the
|
|
144
|
+
// defining file itself -- redundant with what same-file confirmation already
|
|
145
|
+
// finds there, but harmless: `Linter::functions_called` is a plain HashSet, and
|
|
146
|
+
// inserting an already-present name is a no-op).
|
|
147
|
+
//
|
|
148
|
+
// This is what lets `Linter::pending_passthrough_reversals` (see its doc comment
|
|
149
|
+
// in linter.rs) confirm a pass-through function's count across a file boundary --
|
|
150
|
+
// `loaders.py` defines `load_orders` with a bare `return df`; without this, only
|
|
151
|
+
// `pipeline.py`'s own call site would ever confirm it (via cross-file
|
|
152
|
+
// `self.functions` resolution), while `loaders.py`'s OWN per-file check has no way
|
|
153
|
+
// to know anyone calls it, so it would keep counting its internal origin too --
|
|
154
|
+
// the exact cross-file double-count this field closes. Seeded into each file's
|
|
155
|
+
// `Linter::functions_called` by `load_cross_file_symbols`, BEFORE that file's own
|
|
156
|
+
// statement walk runs, so the existing end-of-walk resolution loop
|
|
157
|
+
// (`check_parsed_module`) picks up a cross-file confirmation with no change to the
|
|
158
|
+
// reversal mechanism itself.
|
|
159
|
+
//
|
|
160
|
+
// Scoped to bare-name calls only (`resolve_delegate_target`'s own resolution
|
|
161
|
+
// capability, the same one `resolve_param_governed_call_sites` already relies on)
|
|
162
|
+
// -- a cross-file `self.<attr>.<method>()` call site is NOT covered (would need
|
|
163
|
+
// `current_self_attrs`-equivalent tracking reconstructed for every file in this
|
|
164
|
+
// lightweight re-scan, not just the `Linter`-based `index_file` pass), so that
|
|
165
|
+
// shape's cross-file confirmation remains a known, narrower follow-up gap; its
|
|
166
|
+
// SAME-FILE confirmation is unaffected and already works.
|
|
167
|
+
#[serde(default)]
|
|
168
|
+
pub(crate) called_functions: HashMap<String, std::collections::HashSet<String>>,
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
// Union schema name -> column list across every file's `schemas` map. First
|
|
@@ -638,6 +665,7 @@ pub(crate) fn finalise_index(
|
|
|
638
665
|
resolve_param_schema_requires(&mut files, &all_schemas);
|
|
639
666
|
resolve_transitive_requires(project_root, &mut files);
|
|
640
667
|
let governed = resolve_param_governed_call_sites(project_root, &files);
|
|
668
|
+
let called_functions = resolve_called_functions(project_root, &files);
|
|
641
669
|
ProjectIndex {
|
|
642
670
|
version: 1,
|
|
643
671
|
files,
|
|
@@ -645,6 +673,7 @@ pub(crate) fn finalise_index(
|
|
|
645
673
|
all_schema_locations,
|
|
646
674
|
call_site_errors: governed.call_site_errors,
|
|
647
675
|
resolved_governed: governed.resolved_governed,
|
|
676
|
+
called_functions,
|
|
648
677
|
}
|
|
649
678
|
}
|
|
650
679
|
|
|
@@ -878,6 +907,291 @@ pub(crate) fn resolve_delegate_target(
|
|
|
878
907
|
None
|
|
879
908
|
}
|
|
880
909
|
|
|
910
|
+
// Project-wide pass: for every direct `x = name(...)` call site anywhere in the
|
|
911
|
+
// project, resolve the callee to its defining (file, func) via `resolve_delegate_target`
|
|
912
|
+
// and record it unconditionally -- see `ProjectIndex.called_functions`'s doc comment
|
|
913
|
+
// for what this is for. Much simpler than `resolve_param_governed_call_sites`/
|
|
914
|
+
// `check_governed_call_site`: no template or literal-value resolution, every call site
|
|
915
|
+
// that resolves at all is recorded.
|
|
916
|
+
pub(crate) fn resolve_called_functions(
|
|
917
|
+
project_root: &Path,
|
|
918
|
+
files: &HashMap<String, IndexEntry>,
|
|
919
|
+
) -> HashMap<String, std::collections::HashSet<String>> {
|
|
920
|
+
let mut called: HashMap<String, std::collections::HashSet<String>> = HashMap::new();
|
|
921
|
+
let has_any_functions = files
|
|
922
|
+
.values()
|
|
923
|
+
.any(|entry| !entry.functions.is_empty() || !entry.class_methods.is_empty());
|
|
924
|
+
if !has_any_functions {
|
|
925
|
+
return called;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
for file_path in files.keys() {
|
|
929
|
+
let Ok(source) = fs::read_to_string(file_path) else {
|
|
930
|
+
continue;
|
|
931
|
+
};
|
|
932
|
+
let Ok(parsed) = parse_module(&source) else {
|
|
933
|
+
continue;
|
|
934
|
+
};
|
|
935
|
+
let module = parsed.into_syntax();
|
|
936
|
+
scan_stmts_for_called_functions(
|
|
937
|
+
&module.body,
|
|
938
|
+
file_path,
|
|
939
|
+
project_root,
|
|
940
|
+
files,
|
|
941
|
+
None,
|
|
942
|
+
&mut called,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
called
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Resolve a `self.<attr>` origin (see `collect_self_attr_origins`) to the file that
|
|
949
|
+
// defines its class: same-file first (a class defined in `from_file` itself), then
|
|
950
|
+
// `imports`/`module_aliases` -- mirroring how a real `Linter`'s `self.class_methods`
|
|
951
|
+
// ends up covering both cases (local class visits plus `import_name`'s cross-file
|
|
952
|
+
// pull), just resolved fresh here since this lightweight re-scan has no `Linter`
|
|
953
|
+
// instance whose state already merged them (see `index_file`, which never calls
|
|
954
|
+
// `load_cross_file_symbols` before its own walk).
|
|
955
|
+
fn resolve_self_attr_class_file(
|
|
956
|
+
from_file: &str,
|
|
957
|
+
origin: &SelfAttrOrigin,
|
|
958
|
+
project_root: &Path,
|
|
959
|
+
files: &HashMap<String, IndexEntry>,
|
|
960
|
+
) -> Option<String> {
|
|
961
|
+
let from_entry = files.get(from_file)?;
|
|
962
|
+
if from_entry.class_methods.contains_key(&origin.class_name) {
|
|
963
|
+
return Some(from_file.to_string());
|
|
964
|
+
}
|
|
965
|
+
let module_name = if origin.is_attribute_call {
|
|
966
|
+
from_entry.module_aliases.get(&origin.resolve_name)
|
|
967
|
+
} else {
|
|
968
|
+
from_entry.imports.get(&origin.resolve_name)
|
|
969
|
+
}?;
|
|
970
|
+
let target_file = resolve_module_file(module_name, project_root, files)?;
|
|
971
|
+
if files
|
|
972
|
+
.get(&target_file)?
|
|
973
|
+
.class_methods
|
|
974
|
+
.contains_key(&origin.class_name)
|
|
975
|
+
{
|
|
976
|
+
Some(target_file)
|
|
977
|
+
} else {
|
|
978
|
+
None
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Traversal shape mirrors `scan_stmts_for_governed_calls`'s If/For/While/With
|
|
983
|
+
// recursion, EXTENDED to also descend into function and class bodies (see the
|
|
984
|
+
// FunctionDef/ClassDef arms below for why: unlike the narrower Feast-specific
|
|
985
|
+
// scanner, this one's whole purpose depends on finding real call sites, which
|
|
986
|
+
// overwhelmingly live inside a function or method body, not at module level).
|
|
987
|
+
//
|
|
988
|
+
// `self_attrs` is `Some` while inside a class body (recomputed fresh at each
|
|
989
|
+
// `Stmt::ClassDef` from that class's own `__init__`, matching a real `Linter`'s
|
|
990
|
+
// per-class `current_self_attrs` scoping -- a nested class does NOT inherit an
|
|
991
|
+
// enclosing class's attrs) and `None` everywhere else (module level, a plain
|
|
992
|
+
// function). It's what lets a `self.<attr>.<method>(...)` call site resolve, the
|
|
993
|
+
// shape `resolve_delegate_target` alone can't cover (it only resolves bare names via
|
|
994
|
+
// imports/module_aliases, with no notion of instance attributes).
|
|
995
|
+
pub(crate) fn scan_stmts_for_called_functions(
|
|
996
|
+
stmts: &[Stmt],
|
|
997
|
+
file_path: &str,
|
|
998
|
+
project_root: &Path,
|
|
999
|
+
files: &HashMap<String, IndexEntry>,
|
|
1000
|
+
self_attrs: Option<&HashMap<String, SelfAttrOrigin>>,
|
|
1001
|
+
called: &mut HashMap<String, std::collections::HashSet<String>>,
|
|
1002
|
+
) {
|
|
1003
|
+
for stmt in stmts {
|
|
1004
|
+
let call = match stmt {
|
|
1005
|
+
Stmt::Expr(expr_stmt) => match &*expr_stmt.value {
|
|
1006
|
+
Expr::Call(c) => Some(c),
|
|
1007
|
+
_ => None,
|
|
1008
|
+
},
|
|
1009
|
+
Stmt::Assign(assign) => match &*assign.value {
|
|
1010
|
+
Expr::Call(c) => Some(c),
|
|
1011
|
+
_ => None,
|
|
1012
|
+
},
|
|
1013
|
+
_ => None,
|
|
1014
|
+
};
|
|
1015
|
+
if let Some(call) = call {
|
|
1016
|
+
let callee_name = match &*call.func {
|
|
1017
|
+
Expr::Name(n) => Some(n.id.as_str()),
|
|
1018
|
+
Expr::Attribute(attr) => Some(attr.attr.as_str()),
|
|
1019
|
+
_ => None,
|
|
1020
|
+
};
|
|
1021
|
+
if let Some(callee_name) = callee_name {
|
|
1022
|
+
if let Some((target_file, target_func)) =
|
|
1023
|
+
resolve_delegate_target(file_path, callee_name, project_root, files)
|
|
1024
|
+
{
|
|
1025
|
+
called.entry(target_file).or_default().insert(target_func);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// self.<attr>.<method>(...) -- a shape resolve_delegate_target can't
|
|
1029
|
+
// cover (bare-name imports/module_aliases only, no notion of instance
|
|
1030
|
+
// attributes). Resolved via `self_attrs`, this class's own `__init__`
|
|
1031
|
+
// origins (see the ClassDef arm below).
|
|
1032
|
+
if let (Expr::Attribute(attr), Some(attrs)) = (&*call.func, self_attrs) {
|
|
1033
|
+
if let Expr::Attribute(recv_attr) = &*attr.value {
|
|
1034
|
+
if let Expr::Name(recv_base) = &*recv_attr.value {
|
|
1035
|
+
if recv_base.id.as_str() == "self" {
|
|
1036
|
+
if let Some(origin) = attrs.get(recv_attr.attr.as_str()) {
|
|
1037
|
+
if let Some(target_file) = resolve_self_attr_class_file(
|
|
1038
|
+
file_path,
|
|
1039
|
+
origin,
|
|
1040
|
+
project_root,
|
|
1041
|
+
files,
|
|
1042
|
+
) {
|
|
1043
|
+
let method_known = files
|
|
1044
|
+
.get(&target_file)
|
|
1045
|
+
.and_then(|e| e.class_methods.get(&origin.class_name))
|
|
1046
|
+
.is_some_and(|methods| {
|
|
1047
|
+
methods.contains_key(attr.attr.as_str())
|
|
1048
|
+
});
|
|
1049
|
+
if method_known {
|
|
1050
|
+
called
|
|
1051
|
+
.entry(target_file)
|
|
1052
|
+
.or_default()
|
|
1053
|
+
.insert(attr.attr.to_string());
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
match stmt {
|
|
1063
|
+
Stmt::If(if_stmt) => {
|
|
1064
|
+
scan_stmts_for_called_functions(
|
|
1065
|
+
&if_stmt.body,
|
|
1066
|
+
file_path,
|
|
1067
|
+
project_root,
|
|
1068
|
+
files,
|
|
1069
|
+
self_attrs,
|
|
1070
|
+
called,
|
|
1071
|
+
);
|
|
1072
|
+
for clause in &if_stmt.elif_else_clauses {
|
|
1073
|
+
scan_stmts_for_called_functions(
|
|
1074
|
+
&clause.body,
|
|
1075
|
+
file_path,
|
|
1076
|
+
project_root,
|
|
1077
|
+
files,
|
|
1078
|
+
self_attrs,
|
|
1079
|
+
called,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
Stmt::For(for_stmt) => {
|
|
1084
|
+
scan_stmts_for_called_functions(
|
|
1085
|
+
&for_stmt.body,
|
|
1086
|
+
file_path,
|
|
1087
|
+
project_root,
|
|
1088
|
+
files,
|
|
1089
|
+
self_attrs,
|
|
1090
|
+
called,
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
Stmt::While(while_stmt) => {
|
|
1094
|
+
scan_stmts_for_called_functions(
|
|
1095
|
+
&while_stmt.body,
|
|
1096
|
+
file_path,
|
|
1097
|
+
project_root,
|
|
1098
|
+
files,
|
|
1099
|
+
self_attrs,
|
|
1100
|
+
called,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
Stmt::With(with_stmt) => {
|
|
1104
|
+
scan_stmts_for_called_functions(
|
|
1105
|
+
&with_stmt.body,
|
|
1106
|
+
file_path,
|
|
1107
|
+
project_root,
|
|
1108
|
+
files,
|
|
1109
|
+
self_attrs,
|
|
1110
|
+
called,
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
Stmt::Try(try_stmt) => {
|
|
1114
|
+
scan_stmts_for_called_functions(
|
|
1115
|
+
&try_stmt.body,
|
|
1116
|
+
file_path,
|
|
1117
|
+
project_root,
|
|
1118
|
+
files,
|
|
1119
|
+
self_attrs,
|
|
1120
|
+
called,
|
|
1121
|
+
);
|
|
1122
|
+
for handler in &try_stmt.handlers {
|
|
1123
|
+
let ast::ExceptHandler::ExceptHandler(h) = handler;
|
|
1124
|
+
scan_stmts_for_called_functions(
|
|
1125
|
+
&h.body,
|
|
1126
|
+
file_path,
|
|
1127
|
+
project_root,
|
|
1128
|
+
files,
|
|
1129
|
+
self_attrs,
|
|
1130
|
+
called,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
scan_stmts_for_called_functions(
|
|
1134
|
+
&try_stmt.orelse,
|
|
1135
|
+
file_path,
|
|
1136
|
+
project_root,
|
|
1137
|
+
files,
|
|
1138
|
+
self_attrs,
|
|
1139
|
+
called,
|
|
1140
|
+
);
|
|
1141
|
+
scan_stmts_for_called_functions(
|
|
1142
|
+
&try_stmt.finalbody,
|
|
1143
|
+
file_path,
|
|
1144
|
+
project_root,
|
|
1145
|
+
files,
|
|
1146
|
+
self_attrs,
|
|
1147
|
+
called,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
// Function and class bodies -- deliberately recursed into, UNLIKE
|
|
1151
|
+
// `scan_stmts_for_governed_calls`'s narrower traversal: almost every real
|
|
1152
|
+
// call site lives inside a function or method body, not at module level,
|
|
1153
|
+
// so skipping these (matching the Feast-specific scanner's scope) would
|
|
1154
|
+
// make cross-file confirmation find close to nothing in practice.
|
|
1155
|
+
Stmt::FunctionDef(func_def) => {
|
|
1156
|
+
// `self_attrs` carries through unchanged -- a nested function defined
|
|
1157
|
+
// inside a method still sees the enclosing class's `self.<attr>`
|
|
1158
|
+
// origins, matching a real Linter's `current_self_attrs` staying in
|
|
1159
|
+
// effect for a class's whole body, nested defs included.
|
|
1160
|
+
scan_stmts_for_called_functions(
|
|
1161
|
+
&func_def.body,
|
|
1162
|
+
file_path,
|
|
1163
|
+
project_root,
|
|
1164
|
+
files,
|
|
1165
|
+
self_attrs,
|
|
1166
|
+
called,
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
Stmt::ClassDef(class_def) => {
|
|
1170
|
+
// Fresh per class, from THIS class's own __init__ only -- a nested
|
|
1171
|
+
// class does NOT inherit an enclosing class's attrs, matching
|
|
1172
|
+
// `collect_self_attr_origins`'s same-class scoping in the real Linter.
|
|
1173
|
+
let mut class_self_attrs = HashMap::new();
|
|
1174
|
+
for body_stmt in &class_def.body {
|
|
1175
|
+
if let Stmt::FunctionDef(func_def) = body_stmt {
|
|
1176
|
+
if func_def.name.as_str() == "__init__" {
|
|
1177
|
+
collect_self_attr_origins(&func_def.body, &mut class_self_attrs);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
scan_stmts_for_called_functions(
|
|
1182
|
+
&class_def.body,
|
|
1183
|
+
file_path,
|
|
1184
|
+
project_root,
|
|
1185
|
+
files,
|
|
1186
|
+
Some(&class_self_attrs),
|
|
1187
|
+
called,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
_ => {}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
881
1195
|
// Line/column for a source offset, without needing a full `Linter` instance — used by
|
|
882
1196
|
// resolve_param_governed_call_sites, which parses each file fresh outside any Linter's
|
|
883
1197
|
// own `check_file_internal` pass.
|