diff --git a/.cargo/config.toml b/.cargo/config.toml index 6952380662..9a45726578 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -13,6 +13,7 @@ POLARS_TEST_PATH = { value = "pyrefly/lib/test/polars/third-party", relative = t NARWHALS_TEST_PATH = { value = "pyrefly/lib/test/narwhals/third-party", relative = true } SQLALCHEMY_TEST_PATH = { value = "pyrefly/lib/test/sqlalchemy/third-party", relative = true } GLEAN_SNAPSHOTS_PATH = { value = "pyrefly/lib/report/glean/snapshots", relative = true } +SCIP_SNAPSHOTS_PATH = { value = "pyrefly/lib/report/scip/snapshots", relative = true } COVERAGE_TEST_PATH = { value = "pyrefly/lib/test/coverage/test_files", relative = true } STUBGEN_TEST_PATH = { value = "pyrefly/lib/test/stubgen", relative = true } SHAPE_EXTENSIONS_TEST_PATH = { value = "tensor-shapes/pyrefly-shape-extensions", relative = true } diff --git a/.gitattributes b/.gitattributes index 09e6018f85..2dab05338d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.mdx text eol=lf +pyrefly/lib/report/scip/snapshots/** text eol=lf diff --git a/Cargo.lock b/Cargo.lock index cd1a7fd3d9..1fa1a40236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2296,6 +2296,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "psm" version = "0.1.29" @@ -2358,6 +2378,7 @@ dependencies = [ "paste", "percent-encoding", "pretty_assertions", + "protobuf", "pyrefly_bench_harness", "pyrefly_build", "pyrefly_bundled", @@ -2377,6 +2398,7 @@ dependencies = [ "ruff_python_parser", "ruff_source_file", "ruff_text_size", + "scip", "serde", "serde_json", "serde_repr", @@ -3004,6 +3026,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scip" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f651fbd0f98742a47b58623a7bcfd8fad14455912689e4867a1f43ee7e02c6" +dependencies = [ + "protobuf", +] + [[package]] name = "scopeguard" version = "1.2.0" diff --git a/crates/pyrefly_python/src/ast.rs b/crates/pyrefly_python/src/ast.rs index eaf38ce336..8e9675a9c1 100644 --- a/crates/pyrefly_python/src/ast.rs +++ b/crates/pyrefly_python/src/ast.rs @@ -46,6 +46,7 @@ use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::SourceOrderVisitor; use ruff_python_ast::visitor::source_order::TraversalSignal; use ruff_python_ast::visitor::source_order::walk_node; +use ruff_python_ast::visitor::source_order::walk_stmt; use ruff_python_parser::ParseError; use ruff_python_parser::ParseOptions; use ruff_python_parser::Parsed; @@ -471,6 +472,32 @@ impl Ast { ) } + /// Like `module.visit`, but only calls `f` on expressions that contain `position`, and + /// skips statements that don't contain it. + pub fn visit_covering_exprs<'a>( + module: &'a ModModule, + position: TextSize, + f: &mut dyn FnMut(&'a Expr), + ) { + struct X<'a, 'f> { + position: TextSize, + f: &'f mut dyn FnMut(&'a Expr), + } + impl<'a> SourceOrderVisitor<'a> for X<'a, '_> { + fn visit_stmt(&mut self, x: &'a Stmt) { + if x.range().contains_inclusive(self.position) { + walk_stmt(self, x); + } + } + fn visit_expr(&mut self, x: &'a Expr) { + if x.range().contains_inclusive(self.position) { + (self.f)(x); + } + } + } + X { position, f }.visit_body(&module.body); + } + /// Given a module and a position, find all AST nodes that "cover" the position. /// Return a vector of AST nodes sorted by the node's range, where the "innermost" node that covers the /// position comes first, and parent nodes of that covering node come later. diff --git a/pyrefly/Cargo.toml b/pyrefly/Cargo.toml index 3235e7033e..2041b155e0 100644 --- a/pyrefly/Cargo.toml +++ b/pyrefly/Cargo.toml @@ -66,6 +66,7 @@ parking_lot = { version = "0.12.1", features = ["arc_lock", "send_guard"] } parse-display = "0.8.2" paste = "1.0.14" percent-encoding = "2.1" +protobuf = "3.7.2" pyrefly_build = { path = "../crates/pyrefly_build" } pyrefly_bundled = { path = "../crates/pyrefly_bundled", default-features = false } pyrefly_config = { path = "../crates/pyrefly_config" } @@ -83,6 +84,7 @@ ruff_python_ast = { version = "0.0.14", features = ["serde"] } ruff_python_parser = "0.0.14" ruff_source_file = "0.0.14" ruff_text_size = "0.0.14" +scip = "0.10.0" serde = { version = "1.0.229", features = ["derive", "rc"] } serde_json = { version = "1.0.154", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } serde_repr = "0.1.21" diff --git a/pyrefly/lib/binding/class.rs b/pyrefly/lib/binding/class.rs index 0ba1bf31f3..85c407102a 100644 --- a/pyrefly/lib/binding/class.rs +++ b/pyrefly/lib/binding/class.rs @@ -261,7 +261,7 @@ impl<'a> BindingsBuilder<'a> { let mut pydantic_config_dict = PydanticConfigDict::default(); let docstring_range = Docstring::range_from_stmts(x.body.as_slice()); let body = mem::take(&mut x.body); - let field_docstrings = self.extract_field_docstrings(&body); + let field_docstrings = Self::extract_field_docstrings(&body); let pydantic_before_validator_fields = self.extract_field_validator_fields(&body); let mut attrs_decorators = AttrsDecoratorMethods::default(); collect_attrs_decorator_methods(&body, &mut attrs_decorators); @@ -647,8 +647,7 @@ impl<'a> BindingsBuilder<'a> { } /// Extracts docstrings for each field, mapping the field's range to the docstring's range. - fn extract_field_docstrings( - &self, + pub fn extract_field_docstrings( body: &[ruff_python_ast::Stmt], ) -> SmallMap { use ruff_python_ast::Expr; diff --git a/pyrefly/lib/commands/check.rs b/pyrefly/lib/commands/check.rs index 63dd571618..67b629ddb9 100644 --- a/pyrefly/lib/commands/check.rs +++ b/pyrefly/lib/commands/check.rs @@ -334,6 +334,15 @@ struct OutputArgs { /// stdlib then record those source files instead of stub paths. Affects Glean output only. #[arg(long, value_name = "DIR", requires = "report_glean")] glean_stdlib_root: Option, + /// Generate a SCIP index for the project + #[arg(long, value_name = "OUTPUT_FILE")] + report_scip: Option, + /// Package name for the project's symbols in the SCIP index. Defaults to the name in `pyproject.toml`. + #[arg(long, requires = "report_scip")] + report_scip_project_name: Option, + /// Package version for the project's symbols in the SCIP index. Defaults to the version in `pyproject.toml`. + #[arg(long, requires = "report_scip")] + report_scip_project_version: Option, /// Generate a Pysa-compatible JSON file for each module. Pysa treats every type error /// as a failure, so this defaults `--min-severity` to `error`. #[arg(long, value_name = "OUTPUT_FILE")] @@ -506,6 +515,9 @@ impl OutputArgs { report_glean_ownership, glean_module_names, glean_stdlib_root, + report_scip, + report_scip_project_name, + report_scip_project_version, report_pysa, report_pysa_format, report_demand_tree, @@ -552,6 +564,9 @@ impl OutputArgs { || *report_glean_ownership || glean_module_names.is_some() || glean_stdlib_root.is_some() + || report_scip.is_some() + || report_scip_project_name.is_some() + || report_scip_project_version.is_some() || report_pysa.is_some() || !matches!(report_pysa_format, report::pysa::PysaFormat::Capnp) || report_demand_tree.is_some() @@ -1601,7 +1616,7 @@ impl IncrementalCheckCommand { ) }); let defaults = args.output.resolve(config.as_deref()); - let run = args.prepare_cli_run(timings, transaction, handles, &defaults)?; + let run = args.prepare_cli_run(timings, transaction, version, handles, &defaults)?; check.run(|transaction, handles, sourcedb_errors| { let result = args.finish_cli_run( run, @@ -1648,6 +1663,7 @@ struct PreparedCliRun { memory_trace: MemoryUsageTrace, demand_tree_subscriber: Option, type_check_start: Instant, + scip: Option>>>, } impl CheckArgs { @@ -1802,7 +1818,8 @@ impl CheckArgs { || self.output.report_trace.is_some() || self.output.report_glean.is_some(); RequireLevels { - specified: if retain { + // SCIP solves the dependencies it refers to once the indexed files are written. + specified: if retain || self.output.report_scip.is_some() { Require::Everything } else { Require::Errors @@ -1831,7 +1848,7 @@ impl CheckArgs { require: Require, upsell: UpsellDecision, ) -> anyhow::Result<(CommandExitStatus, Vec)> { - let run = self.prepare_cli_run(timings, transaction, handles, defaults)?; + let run = self.prepare_cli_run(timings, transaction, version, handles, defaults)?; transaction.run(handles, require, None); self.finish_cli_run( run, @@ -1848,6 +1865,7 @@ impl CheckArgs { &self, timings: Timings, transaction: &mut Transaction, + version: &str, handles: &[Handle], defaults: &OutputDefaults, ) -> anyhow::Result { @@ -1908,6 +1926,41 @@ impl CheckArgs { transaction.set_cinderx_reporter(Some(cinderx_reporter)); } + // Each indexed file is written as soon as it is solved, while its answers are still in + // memory, so they don't have to be kept for the whole run. + let scip = match &self.output.report_scip { + Some(path) => { + let relative_to = self.output.relative_to.as_ref().map_or_else( + || std::env::current_dir().ok().unwrap_or_default(), + |x| PathBuf::from_str(x.as_str()).unwrap(), + ); + let project = report::scip::Package::project( + &relative_to, + self.output.report_scip_project_name.clone(), + self.output.report_scip_project_version.clone(), + )?; + let scip = Arc::new(report::scip::ScipWriter::new( + BufWriter::new(File::create(path)?), + handles, + &relative_to, + project, + version, + )?); + let hook_scip = scip.dupe(); + transaction.set_solutions_hook(Some(Box::new(move |handle, transaction| { + if let Err(e) = hook_scip.write_document(transaction, handle) { + error!( + "while writing SCIP document for `{}`: {e:#}", + handle.module() + ); + } + }))); + transaction.set_evict_after_solutions_hook(true); + Some(scip) + } + None => None, + }; + let type_check_start = Instant::now(); let demand_tree_subscriber = if self.output.report_demand_tree.is_some() { transaction.set_demand_collector(Some(DemandCollector::new())); @@ -1924,6 +1977,7 @@ impl CheckArgs { memory_trace, demand_tree_subscriber, type_check_start, + scip, }) } @@ -1942,6 +1996,7 @@ impl CheckArgs { mut memory_trace, demand_tree_subscriber, type_check_start, + scip, } = run; transaction.set_subscriber(None); @@ -2351,6 +2406,16 @@ impl CheckArgs { )?; } } + if let Some(scip) = scip + && let Some(path) = &self.output.report_scip + { + transaction.set_solutions_hook(None); + transaction.set_evict_after_solutions_hook(false); + let scip = Arc::into_inner(scip).expect("the solutions hook holds the only other copy"); + scip.finish(transaction) + .and_then(|mut writer| Ok(writer.flush()?)) + .with_context(|| format!("while writing SCIP index to `{}`", path.display()))?; + } if let Some(pysa_reporter) = transaction.take_pysa_reporter() { report::pysa::write_project_file(&pysa_reporter, transaction, handles, &output_errors)?; } @@ -2403,6 +2468,7 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; + use protobuf::Message as _; use pyrefly_config::config::ConfigScope; use pyrefly_python::module::Module; use pyrefly_python::module_name::ModuleName; @@ -2659,6 +2725,57 @@ def go(w: Widget) -> int: assert!(!facts.contains("main.Widget")); } + #[test] + fn check_writes_scip_report() { + let temp = TempDir::new().unwrap(); + let root = temp.path().join("project"); + fs::create_dir(&root).unwrap(); + let module = root.join("main.py"); + fs::write(&module, "x = 1\n").unwrap(); + let scip_path = temp.path().join("index.scip"); + + let args = CheckArgs::parse_from([ + "check", + "--report-scip", + scip_path.to_str().unwrap(), + "--report-scip-project-name", + "demo", + "--relative-to", + root.to_str().unwrap(), + ]); + let (status, errors, _) = args + .run_once( + "test", + Box::new(TestIncludes { + root: root.clone(), + initial_files: vec![module], + }), + test_config_finder(&root), + UpsellDecision::Skip, + ThreadCount::Inline, + ) + .unwrap(); + assert!(errors.is_empty(), "unexpected type errors: {errors:#?}"); + assert!(matches!(status, CommandExitStatus::Success)); + + let index = scip::types::Index::parse_from_bytes(&fs::read(&scip_path).unwrap()).unwrap(); + assert_eq!(index.documents.len(), 1); + assert_eq!(index.documents[0].relative_path, "main.py"); + assert_eq!( + index.documents[0].occurrences[0].symbol, + "scip-python python demo . main/__init__:" + ); + let metadata = index.metadata.unwrap(); + assert_eq!(metadata.tool_info.name, "scip-pyrefly"); + assert_eq!(metadata.tool_info.version, "test"); + assert!( + metadata.project_root.starts_with("file://") + && metadata.project_root.ends_with("/project/"), + "unexpected project root: {}", + metadata.project_root + ); + } + #[test] fn incremental_check_commits_after_glean_loads_source_dependency() { let temp = TempDir::new().unwrap(); diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index 8c290c0b38..911fe91f4b 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -5981,6 +5981,7 @@ impl Server { HoverOptions { show_go_to_links, verbosity_level, + field_docstring_at_definition: false, }, )) } diff --git a/pyrefly/lib/lsp/wasm/hover.rs b/pyrefly/lib/lsp/wasm/hover.rs index 64c38f0ca2..27531c090b 100644 --- a/pyrefly/lib/lsp/wasm/hover.rs +++ b/pyrefly/lib/lsp/wasm/hover.rs @@ -59,6 +59,7 @@ use vec1::Vec1; use crate::alt::answers_solver::AnswersSolver; use crate::binding::binding::Key; +use crate::binding::bindings::BindingsBuilder; use crate::error::error::Error; use crate::export::exports::ExportLocation; use crate::lsp::module_helpers::collect_symbol_def_paths; @@ -107,6 +108,9 @@ pub struct HoverResult { pub struct HoverOptions { pub show_go_to_links: bool, pub verbosity_level: usize, + /// Whether hovering the definition of a class field shows the docstring that follows it. + /// Hovering a use of the field always does. + pub field_docstring_at_definition: bool, } /// Hover qualifies the module of a value's type, but not of a signature, where a @@ -1470,6 +1474,24 @@ fn resolve_hover_parameter_doc( None } +/// The docstring after a class field's definition, if `position` is on the field's name there. +fn field_docstring_at_definition( + ast: &ModModule, + module: Module, + position: TextSize, +) -> Option { + let nodes = Ast::locate_node(ast, position); + let AnyNodeRef::ExprName(name) = nodes.first()? else { + return None; + }; + let class = nodes.iter().find_map(|node| match node { + AnyNodeRef::StmtClassDef(class) => Some(*class), + _ => None, + })?; + let range = *BindingsBuilder::extract_field_docstrings(&class.body).get(&name.range)?; + Some(Docstring(range, module)) +} + fn is_constructor_dunder(name: Option<&str>) -> bool { name.is_some_and(|name| name == dunder::INIT.as_str() || name == dunder::NEW.as_str()) } @@ -1538,6 +1560,7 @@ pub fn get_hover( HoverOptions { show_go_to_links, verbosity_level: 0, + field_docstring_at_definition: false, }, ) .map(|result| result.hover) @@ -1688,7 +1711,13 @@ pub fn get_hover_with_verbosity( docstring.or_else(|| class_docstring_for_constructor(transaction, handle, position)) } else { docstring - }; + } + .or_else(|| { + options + .field_docstring_at_definition + .then(|| field_docstring_at_definition(ast.as_deref()?, module_info?, position)) + .flatten() + }); let name = name.or_else(|| identifier_text_at(transaction, handle, position)); diff --git a/pyrefly/lib/lsp/wasm/signature_help.rs b/pyrefly/lib/lsp/wasm/signature_help.rs index 8263f44721..bb85cfe956 100644 --- a/pyrefly/lib/lsp/wasm/signature_help.rs +++ b/pyrefly/lib/lsp/wasm/signature_help.rs @@ -18,6 +18,7 @@ use lsp_types::ParameterInformationLabel; use lsp_types::SignatureHelp; use lsp_types::SignatureInformation; use pyrefly_build::handle::Handle; +use pyrefly_python::ast::Ast; use pyrefly_python::docstring::Docstring; use pyrefly_python::docstring::parse_parameter_documentation; use pyrefly_python::module::Module; @@ -197,7 +198,9 @@ impl Transaction<'_> { let parsed_module = self.get_parsed_module(handle)?; let mod_module = parsed_module.module(); let mut res = None; - mod_module.visit(&mut |x| Self::visit_finding_signature_range(x, position, &mut res)); + Ast::visit_covering_exprs(&mod_module, position, &mut |x| { + Self::visit_finding_signature_range(x, position, &mut res) + }); let (call, mut active_argument, provided_arg_ranges) = res?; let callee_range = call.func.range(); let call_args_range = call.arguments.range; diff --git a/pyrefly/lib/report.rs b/pyrefly/lib/report.rs index ea5b3bf323..fe40d5a372 100644 --- a/pyrefly/lib/report.rs +++ b/pyrefly/lib/report.rs @@ -11,4 +11,5 @@ pub mod debug_info; pub mod dependency_graph; pub mod glean; pub mod pysa; +pub mod scip; pub mod trace; diff --git a/pyrefly/lib/report/glean/convert.rs b/pyrefly/lib/report/glean/convert.rs index 2216ef97f8..60a7d81e44 100644 --- a/pyrefly/lib/report/glean/convert.rs +++ b/pyrefly/lib/report/glean/convert.rs @@ -243,7 +243,9 @@ fn stdlib_source_file(root: &str, stub_path: &Path) -> src::File { )) } -fn gather_nonlocal_variables(body: &[Stmt]) -> (Arc>, Arc>) { +pub(crate) fn gather_nonlocal_variables( + body: &[Stmt], +) -> (Arc>, Arc>) { let mut globals = SmallSet::new(); let mut nonlocals = SmallSet::new(); for stmt in body { diff --git a/pyrefly/lib/report/scip.rs b/pyrefly/lib/report/scip.rs new file mode 100644 index 0000000000..2ee4206418 --- /dev/null +++ b/pyrefly/lib/report/scip.rs @@ -0,0 +1,1141 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::collections::HashMap; +use std::io::Write; +use std::iter; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + +use dupe::Dupe; +use lsp_types::Contents; +use lsp_types::Hover; +use lsp_types::Uri; +use protobuf::Message as _; +use pyrefly_build::handle::Handle; +use pyrefly_python::ast::Ast; +use pyrefly_python::docstring::Docstring; +use pyrefly_python::module::Module; +use pyrefly_python::module_name::ModuleName; +use pyrefly_python::module_path::ModulePathDetails; +use pyrefly_python::symbol_kind::SymbolKind; +use pyrefly_python::sys_info::SysInfo; +use pyrefly_util::absolutize::Absolutize; +use pyrefly_util::fs_anyhow; +use pyrefly_util::lined_buffer::DisplayPos; +use pyrefly_util::lined_buffer::DisplayRange; +use pyrefly_util::lock::Mutex; +use pyrefly_util::unix_path::path_to_unix_string; +use pyrefly_util::visit::Visit; +use rayon::prelude::*; +use ruff_python_ast::Expr; +use ruff_python_ast::ExprContext; +use ruff_python_ast::Identifier; +use ruff_python_ast::ModModule; +use ruff_python_ast::Stmt; +use ruff_python_ast::StmtClassDef; +use ruff_python_ast::name::Name; +use ruff_text_size::Ranged; +use ruff_text_size::TextRange; +use ruff_text_size::TextSize; +use scip::symbol::format_symbol; +use scip::types::Descriptor; +use scip::types::Document; +use scip::types::Index; +use scip::types::Metadata; +use scip::types::Occurrence; +use scip::types::PositionEncoding; +use scip::types::Relationship; +use scip::types::Symbol; +use scip::types::SymbolInformation; +use scip::types::SymbolRole; +use scip::types::TextEncoding; +use scip::types::ToolInfo; +use scip::types::descriptor::Suffix; +use starlark_map::small_map::SmallMap; +use starlark_map::small_set::SmallSet; + +use crate::config::config::ConfigFile; +use crate::lsp::wasm::hover::HoverOptions; +use crate::lsp::wasm::hover::HoverResult; +use crate::lsp::wasm::hover::get_hover_with_verbosity; +use crate::report::scip::symbols::ScipSymbol; +use crate::report::scip::symbols::definitions; +use crate::report::scip::symbols::descriptor; +use crate::state::lsp::CalleeKind; +use crate::state::lsp::FindDefinitionItemWithDocstring; +use crate::state::lsp::FindPreference; +use crate::state::lsp::callee_kind_from_call; +use crate::state::require::Require; +use crate::state::state::Transaction; +use crate::types::globals::ImplicitGlobal; + +#[cfg(test)] +mod snapshot; +mod symbols; + +const SCHEME: &str = "scip-python"; +const MANAGER: &str = "python"; + +#[derive(Debug, Clone)] +pub struct Package { + pub name: String, + pub version: String, +} + +impl Package { + /// The package of the project's own symbols. Explicit values come first, then `[project]` + /// and `[tool.poetry]` in the project root's `pyproject.toml`. + pub fn project( + project_root: &Path, + name: Option, + version: Option, + ) -> anyhow::Result { + let pyproject = project_root.join(ConfigFile::PYPROJECT_FILE_NAME); + let pyproject: toml::Table = if (name.is_none() || version.is_none()) && pyproject.exists() + { + fs_anyhow::read_to_string(&pyproject)?.parse()? + } else { + toml::Table::new() + }; + let lookup = |key: &str| { + let poetry = pyproject.get("tool").and_then(|tool| tool.get("poetry")); + [pyproject.get("project"), poetry] + .into_iter() + .flatten() + .find_map(|table| Some(table.get(key)?.as_str()?.to_owned())) + }; + // An empty name or version is written as `.` in symbols. + Ok(Self { + name: name.or_else(|| lookup("name")).unwrap_or_default(), + version: version.or_else(|| lookup("version")).unwrap_or_default(), + }) + } +} + +/// Writes a SCIP index of `handles` after they have been checked. +#[cfg(test)] +fn write_scip( + writer: &mut (impl Write + Send), + transaction: &mut Transaction, + handles: &[Handle], + project_root: &Path, + project: &Package, + version: &str, +) -> anyhow::Result<()> { + let scip = ScipWriter::new(writer, handles, project_root, project.clone(), version)?; + transaction.install(|| { + handles + .par_iter() + .try_for_each(|handle| scip.write_document(transaction, handle)) + })?; + scip.finish(transaction)?; + Ok(()) +} + +/// Writes a SCIP index as a stream of `Index` messages, which a reader merges into one: the +/// metadata, then each document as soon as it is built, in no particular order, then the +/// external symbols. Documents can be written while the files are being checked, from a +/// solutions hook. +pub struct ScipWriter { + writer: Mutex, + handles: SmallSet, + project_root: PathBuf, + project: Package, + definitions: Mutex>>>, + asts: Mutex>>, + distributions: Mutex>>>, + external_symbols: Mutex>, +} + +impl ScipWriter { + pub fn new( + mut writer: W, + handles: &[Handle], + project_root: &Path, + project: Package, + version: &str, + ) -> anyhow::Result { + let project_root = project_root.absolutize(); + Index { + metadata: Some(Metadata { + project_root: Uri::from_directory_path(&project_root) + .map(|uri| uri.to_string()) + .unwrap_or_default(), + tool_info: Some(ToolInfo { + name: "scip-pyrefly".to_owned(), + version: version.to_owned(), + ..Default::default() + }) + .into(), + text_document_encoding: TextEncoding::UTF8.into(), + ..Default::default() + }) + .into(), + ..Default::default() + } + .write_to_writer(&mut writer)?; + Ok(Self { + writer: Mutex::new(writer), + handles: handles.iter().cloned().collect(), + project_root, + project, + definitions: Mutex::new(HashMap::new()), + asts: Mutex::new(HashMap::new()), + distributions: Mutex::new(HashMap::new()), + external_symbols: Mutex::new(HashMap::new()), + }) + } + + fn indexer<'a>(&'a self, transaction: &'a Transaction<'a>) -> Indexer<'a> { + Indexer { + transaction, + project_root: &self.project_root, + project: &self.project, + definitions: &self.definitions, + asts: &self.asts, + distributions: &self.distributions, + external_symbols: &self.external_symbols, + } + } + + /// Writes the document of `handle`, if it is one of the indexed files. + pub fn write_document(&self, transaction: &Transaction, handle: &Handle) -> anyhow::Result<()> { + if !self.handles.contains(handle) { + return Ok(()); + } + let Some(document) = self.indexer(transaction).document(handle) else { + return Ok(()); + }; + let bytes = Index { + documents: vec![document], + ..Default::default() + } + .write_to_bytes()?; + self.writer.lock().write_all(&bytes)?; + Ok(()) + } + + /// Writes the external symbols, once every document is written. + pub fn finish(self, transaction: &mut Transaction) -> anyhow::Result { + let Self { + writer, + definitions, + external_symbols, + .. + } = self; + let mut writer = writer.into_inner(); + let mut first_uses: Vec<(String, ExternalUse)> = + external_symbols.into_inner().into_iter().collect(); + // A keyword argument is documented with its parameter's type, which comes from the + // defining module's answers. Those are only kept for modules solved to + // `Require::Everything`, so the symbol is documented at its definition instead. + let dependencies: Vec = definitions + .into_inner() + .into_keys() + .filter(|x| transaction.get_ast(x).is_none()) + .collect(); + transaction.run(&dependencies, Require::Everything, None); + let transaction = &*transaction; + transaction.install(|| { + first_uses.par_iter_mut().for_each(|(_, first_use)| { + if first_use.documentation.is_empty() + && let Some((handle, position)) = &first_use.definition + { + first_use.documentation = documentation(transaction, handle, *position); + } + }) + }); + let mut external_symbols: Vec = first_uses + .into_iter() + .filter(|(_, first_use)| !first_use.documentation.is_empty()) + .map(|(symbol, first_use)| SymbolInformation { + symbol, + documentation: first_use.documentation, + ..Default::default() + }) + .collect(); + external_symbols.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + Index { + external_symbols, + ..Default::default() + } + .write_to_writer(&mut writer)?; + Ok(writer) + } +} + +/// Top-level names whose kind in the bundled stubs differs from how other indexers name them: +/// `Any` was a variable in older stubs, and `NotImplemented` and `Ellipsis` are named as types. +const SPECIAL_FORMS: &[(&str, &str, Suffix)] = &[ + ("typing", "Any", Suffix::Term), + ("builtins", "NotImplemented", Suffix::Type), + ("builtins", "Ellipsis", Suffix::Type), +]; + +struct Indexer<'a> { + transaction: &'a Transaction<'a>, + project_root: &'a Path, + project: &'a Package, + /// The definitions of every module seen so far, keyed by the range of the defining identifier. + definitions: &'a Mutex>>>, + /// The parsed AST of every dependency seen so far, since only the indexed files keep theirs. + asts: &'a Mutex>>, + /// The installed distributions of every site-packages directory seen so far. + distributions: &'a Mutex>>>, + /// The first use, in path and position order, of every symbol defined outside the project and + /// referenced so far. The symbol is documented with the hover there, so its documentation + /// doesn't depend on the order documents are built in. + external_symbols: &'a Mutex>, +} + +/// For a symbol defined outside the project, where to document it when the hover at its use has +/// nothing. +type External = Option>; + +struct ExternalUse { + relative_path: PathBuf, + position: TextSize, + documentation: Vec, + /// Where the symbol is defined, to document it there when the hover at the use has nothing. + definition: Option<(Handle, TextSize)>, +} + +impl Indexer<'_> { + fn document(&self, handle: &Handle) -> Option { + let module = self.transaction.get_module_info(handle)?; + let relative_path = module + .path() + .as_path() + .strip_prefix(self.project_root) + .ok()?; + let ast = self.transaction.get_ast(handle)?; + let definitions = self.definitions(&module, handle.sys_info().dupe())?; + let mut occurrences = vec![Occurrence { + range: vec![0, 0, 0], + symbol: module_symbol(self.project, module.name()), + symbol_roles: SymbolRole::Definition as i32, + ..Default::default() + }]; + let mut enclosing_ranges = SmallMap::new(); + ast.body + .iter() + .for_each(|x| collect_enclosing_ranges(x, &mut enclosing_ranges)); + // Only the first binding of a symbol defines it, and later ones refer to it. A parameter + // is defined again by each overload. + let mut parameters = SmallSet::new(); + ast.body + .iter() + .for_each(|x| collect_parameters(x, &mut parameters)); + let mut defined = SmallSet::new(); + occurrences.extend(definitions.iter().map(|(range, symbol)| { + let symbol = format_scip_symbol(self.project, symbol); + let is_definition = defined.insert(symbol.clone()) || parameters.contains(range); + Occurrence { + range: scip_range(&module, *range), + symbol, + symbol_roles: if is_definition { + SymbolRole::Definition as i32 + } else { + 0 + }, + enclosing_range: enclosing_ranges + .get(range) + .filter(|_| is_definition) + .map_or_else(Vec::new, |x| scip_range(&module, *x)), + ..Default::default() + } + })); + + // Names that resolve to no definition get one local per name, numbered after the + // module's own locals. + let first_unresolved = definitions + .values() + .filter_map(|symbol| match symbol { + ScipSymbol::Local(id) => Some(id + 1), + ScipSymbol::Global(_) => None, + }) + .max() + .unwrap_or(0); + let mut unresolved: SmallMap = SmallMap::new(); + let mut uses = Vec::new(); + ast.visit(&mut |x| collect_expr_uses(x, &mut uses)); + ast.body.iter().for_each(|x| collect_imports(x, &mut uses)); + // The first `self.x = ...` of an attribute is its definition, not a use. + uses.retain(|x| !definitions.contains_key(&x.range)); + uses.sort_by_key(|x| x.range.start()); + for x in uses { + let symbol = match (self.reference(handle, &module, &x), &x.kind) { + (Some((symbol, external)), _) => { + if let Some(definition) = external { + let position = use_position(&x); + let is_first = |external_symbols: &HashMap| { + external_symbols.get(&symbol).is_none_or(|first_use| { + (relative_path, position) + < (first_use.relative_path.as_path(), first_use.position) + }) + }; + // Documented here, while this file's answers are still in memory. + if is_first(&self.external_symbols.lock()) { + let documentation = documentation(self.transaction, handle, position); + let mut external_symbols = self.external_symbols.lock(); + if is_first(&external_symbols) { + external_symbols.insert( + symbol.clone(), + ExternalUse { + relative_path: relative_path.to_owned(), + position, + documentation, + definition, + }, + ); + } + } + } + symbol + } + (None, UseKind::Name | UseKind::Import) => { + let id = first_unresolved + unresolved.len(); + format!("local {}", unresolved.entry(x.identifier.id).or_insert(id)) + } + // An unresolved attribute or keyword isn't a variable, so it gets no local. + (None, UseKind::Attribute | UseKind::Keyword(_)) => continue, + }; + occurrences.push(Occurrence { + range: scip_range(&module, x.range), + symbol, + ..Default::default() + }); + } + // A symbol defined more than once is documented at its first definition. + let mut symbols: SmallMap = SmallMap::new(); + let mut module_documentation = format!("```python\n(module) {}\n```", module.name()); + if let Some(range) = Docstring::range_from_stmts(&ast.body) { + module_documentation.push_str("\n---\n"); + module_documentation.push_str(Docstring(range, module.dupe()).resolve().trim()); + } + let module_symbol = module_symbol(self.project, module.name()); + symbols.insert( + module_symbol.clone(), + SymbolInformation { + symbol: module_symbol, + documentation: vec![module_documentation], + ..Default::default() + }, + ); + for (range, symbol) in definitions.iter() { + let symbol = format_scip_symbol(self.project, symbol); + if !symbols.contains_key(&symbol) { + let documentation = documentation(self.transaction, handle, range.start()); + symbols.insert( + symbol.clone(), + SymbolInformation { + symbol, + documentation, + ..Default::default() + }, + ); + } + } + let mut classes = Vec::new(); + ast.body + .iter() + .for_each(|x| collect_classes(x, &mut classes)); + for class in classes { + for (range, implemented) in self.implementations(handle, &module, class) { + let Some(symbol) = definitions.get(&range) else { + continue; + }; + if let Some(info) = symbols.get_mut(&format_scip_symbol(self.project, symbol)) { + info.relationships = implemented + .into_iter() + .map(|symbol| Relationship { + symbol, + is_implementation: true, + ..Default::default() + }) + .collect(); + } + } + } + let symbols = symbols + .into_values() + .filter(|info| !info.documentation.is_empty() || !info.relationships.is_empty()) + .collect(); + Some(Document { + language: "python".to_owned(), + relative_path: path_to_unix_string(relative_path), + position_encoding: PositionEncoding::UTF32CodeUnitOffsetFromLineStart.into(), + occurrences, + symbols, + ..Default::default() + }) + } + + /// The symbols that a class and its methods implement, keyed by the range of the defining + /// name. A class implements each of its bases, and a method implements the first method of + /// the same name found through the bases, in order. A method whose name starts with `__` + /// implements nothing. + fn implementations( + &self, + handle: &Handle, + module: &Module, + class: &StmtClassDef, + ) -> Vec<(TextRange, Vec)> { + let bases: Vec = class + .bases() + .iter() + .filter_map(|base| { + let base = match base { + Expr::Subscript(x) => &*x.value, + x => x, + }; + let (identifier, kind) = match base { + Expr::Name(x) => (Ast::expr_name_identifier(x.clone()), UseKind::Name), + Expr::Attribute(x) => (x.attr.clone(), UseKind::Attribute), + _ => return None, + }; + Some(Use { + range: base.range(), + identifier, + kind, + }) + }) + .collect(); + let class_bases: Vec<_> = bases + .iter() + .filter_map(|base| { + self.reference(handle, module, base) + .map(|(symbol, _)| symbol) + }) + .collect(); + let mut implementations = vec![(class.name.range, class_bases)]; + for x in &class.body { + let Stmt::FunctionDef(method) = x else { + continue; + }; + let name = &method.name.id; + if name.starts_with("__") { + continue; + } + let implemented = bases.iter().find_map(|base| { + let definitions = self + .transaction + .find_definition_for_attribute(handle, base.range, name, preference()) + .ok()?; + let definition = definitions.into_iter().next()?; + self.symbol(handle, module, definition) + .map(|(symbol, _)| symbol) + }); + implementations.push((method.name.range, implemented.into_iter().collect())); + } + implementations.retain(|(_, implemented)| !implemented.is_empty()); + implementations + } + + /// The symbol a use refers to, and where it is defined if that is outside the project, or + /// `None` if it has no definition we can name. + fn reference(&self, handle: &Handle, module: &Module, x: &Use) -> Option<(String, External)> { + let preference = preference(); + let definition = match &x.kind { + UseKind::Name => { + let definition = self + .transaction + .find_definition_for_name_use(handle, &x.identifier, preference) + .ok() + .flatten(); + // Implicit module globals like `__name__` have no definition, so they are named + // as stdlib builtins. + if definition.is_none() + && ImplicitGlobal::implicit_globals(false).any(|g| *g.name() == x.identifier.id) + { + let descriptors = vec![ + descriptor("builtins", Suffix::Namespace), + descriptor(&x.identifier.id, Suffix::Type), + ]; + return Some(( + global_symbol(&stdlib_package(handle), descriptors), + Some(None), + )); + } + definition + } + UseKind::Keyword(callee_kind) => { + // Dependencies keep no AST, so the callee's module comes from our cache rather + // than being parsed again for every keyword. + let definition = self + .transaction + .find_definition_for_keyword_argument_with_ast( + handle, + &x.identifier, + callee_kind, + preference, + |module| { + let defining = Handle::new( + module.name(), + module.path().dupe(), + handle.sys_info().dupe(), + ); + self.ast(module, &defining) + }, + ) + .into_iter() + .next()?; + Some(FindDefinitionItemWithDocstring { + metadata: definition.metadata, + definition_range: definition.definition_range, + module: definition.module, + docstring_range: None, + display_name: Some(x.identifier.id.to_string()), + }) + } + UseKind::Import | UseKind::Attribute => { + let definitions = self + .transaction + .find_definition(handle, use_position(x), preference) + .ok()?; + definitions.into_iter().next() + } + }?; + self.symbol(handle, module, definition) + } + + /// The symbol of a definition, and whether it is defined outside the project, or `None` if + /// it is a local of another module or has no definition we can name. + fn symbol( + &self, + handle: &Handle, + module: &Module, + definition: FindDefinitionItemWithDocstring, + ) -> Option<(String, External)> { + let external = (!self.in_project(&definition.module, handle)).then(|| { + let defining = Handle::new( + definition.module.name(), + definition.module.path().dupe(), + handle.sys_info().dupe(), + ); + Some((defining, definition.definition_range.start())) + }); + let package = self.package(&definition.module, handle); + // A submodule accessed as an attribute resolves to the empty range at the module's start. + if definition.metadata.symbol_kind() == Some(SymbolKind::Module) + || definition.definition_range.is_empty() + { + return Some(( + module_symbol(&package, definition.module.name()), + external.map(|_| None), + )); + } + let symbols = self.definitions(&definition.module, handle.sys_info().dupe())?; + match symbols.get(&definition.definition_range)? { + ScipSymbol::Global(descriptors) => { + Some((global_symbol(&package, descriptors.clone()), external)) + } + ScipSymbol::Local(id) => { + (definition.module.path() == module.path()).then(|| (format!("local {id}"), None)) + } + } + } + + fn definitions( + &self, + module: &Module, + sys_info: SysInfo, + ) -> Option>> { + let handle = Handle::new(module.name(), module.path().dupe(), sys_info); + if let Some(definitions) = self.definitions.lock().get(&handle) { + return Some(definitions.dupe()); + } + let ast = self.ast(module, &handle); + let mut table: SmallMap<_, _> = definitions(module.name(), &ast).into_iter().collect(); + for (module_name, name, suffix) in SPECIAL_FORMS { + if module.name().as_str() != *module_name { + continue; + } + for symbol in table.values_mut() { + if let ScipSymbol::Global(descriptors) = symbol + && let [_, descriptor] = descriptors.as_mut_slice() + && descriptor.name == *name + { + descriptor.suffix = (*suffix).into(); + } + } + } + let table = Arc::new(table); + self.definitions.lock().insert(handle, table.dupe()); + Some(table) + } + + /// The AST of `module`, parsed again if it is a dependency. + fn ast(&self, module: &Module, handle: &Handle) -> Arc { + if let Some(ast) = self.transaction.get_ast(handle) { + return ast; + } + if let Some(ast) = self.asts.lock().get(handle) { + return ast.dupe(); + } + let ast = Arc::new(Ast::parse(module.contents(), module.source_type()).0); + self.asts.lock().insert(handle.clone(), ast.dupe()); + ast + } + + /// Whether a module referenced from `handle` belongs to the project: it is under the project + /// root, and not in a site-packages directory there, such as a virtual environment's. + fn in_project(&self, module: &Module, handle: &Handle) -> bool { + let path = module.path().as_path(); + path.starts_with(self.project_root) + && !self + .transaction + .get_config(handle) + .iter() + .flat_map(|config| config.site_package_path()) + .any(|root| path.starts_with(root)) + } + + /// The package of a module referenced from `handle`, whose site-packages are searched for + /// installed distributions. + fn package(&self, module: &Module, handle: &Handle) -> Package { + let bundled = matches!( + module.path().details(), + ModulePathDetails::BundledTypeshed(_) + ); + if !bundled && self.in_project(module, handle) { + return self.project.clone(); + } + // Match by top-level module, so that bundled stubs get the installed package too, even for + // a stdlib module like `typing_extensions`. + let top_level = module.name().first_component(); + let config = self.transaction.get_config(handle); + let installed = config + .iter() + .flat_map(|config| config.site_package_path()) + .find_map(|root| { + let installed = self + .distributions + .lock() + .entry(root.clone()) + .or_insert_with(|| Arc::new(distributions(root))) + .dupe(); + installed.get(&top_level).cloned() + }); + installed.unwrap_or_else(|| { + if bundled { + stdlib_package(handle) + } else { + // Without package metadata, name the package after the top-level module. + Package { + name: top_level.to_string(), + version: String::new(), + } + } + }) + } +} + +/// The hover text at a position, as SCIP documentation. +fn documentation(transaction: &Transaction, handle: &Handle, position: TextSize) -> Vec { + let options = HoverOptions { + show_go_to_links: false, + verbosity_level: 0, + field_docstring_at_definition: true, + }; + match get_hover_with_verbosity(transaction, handle, position, options) { + Some(HoverResult { + hover: + Hover { + contents: Contents::MarkupContent(markup), + .. + }, + .. + }) => vec![markup.value], + _ => Vec::new(), + } +} + +fn stdlib_package(handle: &Handle) -> Package { + let version = handle.sys_info().version(); + Package { + name: "python-stdlib".to_owned(), + version: format!("{}.{}", version.major, version.minor), + } +} + +/// The distributions installed in a site-packages directory, keyed by each top-level module they +/// installed. A distribution whose metadata can't be read is left out. +fn distributions(site_packages: &Path) -> HashMap { + let mut modules = HashMap::new(); + let pattern = site_packages.join("*.dist-info"); + for dist_info in glob::glob(&pattern.to_string_lossy()) + .into_iter() + .flatten() + .flatten() + { + let (Ok(metadata), Ok(record)) = ( + fs_anyhow::read_to_string(&dist_info.join("METADATA")), + fs_anyhow::read_to_string(&dist_info.join("RECORD")), + ) else { + continue; + }; + // The header fields end at the first blank line. + let field = |key: &str| { + metadata + .lines() + .take_while(|line| !line.is_empty()) + .find_map(|line| Some(line.strip_prefix(key)?.strip_prefix(':')?.trim().to_owned())) + }; + let (Some(name), Some(version)) = (field("Name"), field("Version")) else { + continue; + }; + let package = Package { name, version }; + // Each RECORD line is a CSV row whose first column is the installed file. Its first path + // component, up to the first `.`, names a top-level module (or a `-stubs` package), unless + // it isn't an identifier, as for the `.dist-info` directory itself. + for line in record.lines() { + let component = line.split(['/', ',']).next().unwrap_or_default(); + let module = component + .split('.') + .next() + .unwrap_or_default() + .trim_end_matches("-stubs"); + if !module.is_empty() && module.chars().all(|c| c.is_alphanumeric() || c == '_') { + modules + .entry(Name::new(module)) + .or_insert_with(|| package.clone()); + } + } + } + modules +} + +/// A name that refers to a definition elsewhere. +struct Use { + /// The range of the occurrence. For a relative module, it also covers the leading dots. + range: TextRange, + identifier: Identifier, + kind: UseKind, +} + +enum UseKind { + /// A name in an expression. + Name, + /// A module or name in an import statement. + Import, + Attribute, + /// The name of a keyword argument in a call to the callee. + Keyword(CalleeKind), +} + +/// Collects every name that is read or deleted, every attribute, and every keyword argument. +fn collect_expr_uses(x: &Expr, uses: &mut Vec) { + let mut push = |identifier: Identifier, kind| { + uses.push(Use { + range: identifier.range, + identifier, + kind, + }) + }; + match x { + Expr::Name(name) if name.ctx != ExprContext::Store => { + push(Ast::expr_name_identifier(name.clone()), UseKind::Name) + } + Expr::Attribute(attribute) => push(attribute.attr.clone(), UseKind::Attribute), + Expr::Call(call) => { + for keyword in call.arguments.keywords.iter() { + if let Some(arg) = &keyword.arg { + push(arg.clone(), UseKind::Keyword(callee_kind_from_call(call))); + } + } + } + _ => {} + } + x.recurse(&mut |x| collect_expr_uses(x, uses)); +} + +/// The position at which a use is resolved. At the identifier's last character, a dotted module +/// name in an import resolves to the whole module rather than to its first component. +fn use_position(x: &Use) -> TextSize { + x.identifier.range.end() - TextSize::from(1) +} + +fn preference() -> FindPreference { + FindPreference { + // A call like `C()` refers to the class `C`, not to `C.__init__`. + resolve_call_dunders: false, + ..Default::default() + } +} + +/// Collects every class definition, including nested ones. +fn collect_classes<'a>(x: &'a Stmt, classes: &mut Vec<&'a StmtClassDef>) { + if let Stmt::ClassDef(x) = x { + classes.push(x); + } + x.recurse(&mut |x| collect_classes(x, classes)); +} + +fn collect_parameters(x: &Stmt, ranges: &mut SmallSet) { + if let Stmt::FunctionDef(x) = x { + ranges.extend(x.parameters.iter().map(|x| x.name().range)); + } + x.recurse(&mut |x| collect_parameters(x, ranges)); +} + +/// Maps the name of every function and class to its whole definition, decorators included. +fn collect_enclosing_ranges(x: &Stmt, ranges: &mut SmallMap) { + match x { + Stmt::FunctionDef(x) => ranges.insert(x.name.range, x.range), + Stmt::ClassDef(x) => ranges.insert(x.name.range, x.range), + _ => None, + }; + x.recurse(&mut |x| collect_enclosing_ranges(x, ranges)); +} + +/// Collects the module and name identifiers of every import statement. +fn collect_imports(x: &Stmt, uses: &mut Vec) { + let mut import = |range, identifier: &Identifier| { + uses.push(Use { + range, + identifier: identifier.clone(), + kind: UseKind::Import, + }) + }; + let aliases = match x { + Stmt::Import(x) => &x.names, + Stmt::ImportFrom(x) => { + if let Some(module) = &x.module { + let dots = TextSize::from(x.level); + import(TextRange::new(module.start() - dots, module.end()), module); + } + &x.names + } + _ => { + x.recurse(&mut |x| collect_imports(x, uses)); + return; + } + }; + for alias in aliases.iter().filter(|alias| alias.name.as_str() != "*") { + for identifier in iter::once(&alias.name).chain(&alias.asname) { + import(identifier.range, identifier); + } + } +} + +/// A SCIP range: `[line, start, end]` on one line, or `[start line, start, end line, end]`. +fn scip_range(module: &Module, range: TextRange) -> Vec { + let DisplayRange { start, end } = module.display_range(range); + let position = |pos: DisplayPos| { + ( + pos.line_within_file().to_zero_indexed() as i32, + pos.column().get() as i32 - 1, + ) + }; + let ((start_line, start_column), (end_line, end_column)) = (position(start), position(end)); + if start_line == end_line { + vec![start_line, start_column, end_column] + } else { + vec![start_line, start_column, end_line, end_column] + } +} + +fn format_scip_symbol(package: &Package, symbol: &ScipSymbol) -> String { + match symbol { + ScipSymbol::Global(descriptors) => global_symbol(package, descriptors.clone()), + ScipSymbol::Local(id) => format!("local {id}"), + } +} + +fn global_symbol(package: &Package, descriptors: Vec) -> String { + // The SCIP spec escapes spaces in package fields by doubling them. + let escape = |x: &str| x.replace(' ', " "); + format_symbol(Symbol { + scheme: SCHEME.to_owned(), + package: Some(scip::types::Package { + manager: MANAGER.to_owned(), + name: escape(&package.name), + version: escape(&package.version), + ..Default::default() + }) + .into(), + descriptors, + ..Default::default() + }) +} + +fn module_symbol(package: &Package, module: ModuleName) -> String { + global_symbol( + package, + vec![ + descriptor(module.as_str(), Suffix::Namespace), + descriptor("__init__", Suffix::Meta), + ], + ) +} + +#[cfg(test)] +mod tests { + use pyrefly_python::module_path::ModulePath; + + use super::*; + use crate::report::scip::snapshot::format_snapshot; + use crate::test::util::TestEnv; + + fn snapshots_dir() -> PathBuf { + let path = std::env::var("SCIP_SNAPSHOTS_PATH") + .expect("SCIP_SNAPSHOTS_PATH env var not set: buck should set this automatically"); + std::env::current_dir().unwrap().join(path) + } + + /// A test case uses its own `pyproject.toml` when it has one. + fn project_package(snapshots: &Path, case: &str) -> Package { + let info: serde_json::Value = serde_json::from_str( + &fs_anyhow::read_to_string(&snapshots.join("packageInfo.json")).unwrap(), + ) + .unwrap(); + let input_dir = snapshots.join("input").join(case); + let package = match info["special"].get(case) { + Some(package) => Some(package), + None if input_dir.join(ConfigFile::PYPROJECT_FILE_NAME).exists() => None, + None => Some(&info["default"]), + }; + let field = |key: &str| package.map(|package| package[key].as_str().unwrap().to_owned()); + Package::project(&input_dir, field("name"), field("version")).unwrap() + } + + fn index(case: &str) -> Index { + let snapshots = snapshots_dir(); + let input_dir = snapshots.join("input").join(case); + // Case-local dependencies model a virtual environment inside the project root. + let site_packages = input_dir.join("site-packages"); + let mut env = TestEnv::new(); + let mut handles = Vec::new(); + for path in glob::glob(&format!("{}/**/*.py", input_dir.display())).unwrap() { + let path = path.unwrap(); + if path.starts_with(&site_packages) { + continue; + } + // As with an import root, modules under `src/` are named without the prefix. + let relative = path + .strip_prefix(input_dir.join("src")) + .or_else(|_| path.strip_prefix(&input_dir)) + .unwrap(); + let name = ModuleName::from_relative_path(relative).unwrap(); + env.add_real_path(name.as_str(), path.clone()); + handles.push(Handle::new( + name, + ModulePath::filesystem(path), + env.sys_info(), + )); + } + let (state, _) = env + .with_site_package_paths(vec![site_packages, snapshots.join("site-packages")]) + .to_state(); + let mut bytes = Vec::new(); + write_scip( + &mut bytes, + &mut state.transaction(), + &handles, + &input_dir, + &project_package(&snapshots, case), + "test", + ) + .unwrap(); + let index = Index::parse_from_bytes(&bytes).unwrap(); + assert_eq!( + index.documents.len(), + handles.len(), + "missing snapshot documents" + ); + index + } + + fn assert_snapshot(case: &str) { + let snapshots = snapshots_dir(); + let input_dir = snapshots.join("input").join(case); + let output_dir = snapshots.join("output").join(case); + let index = index(case); + let update = std::env::var("SCIP_UPDATE_SNAPSHOTS").is_ok(); + for doc in &index.documents { + let input = fs_anyhow::read_to_string(&input_dir.join(&doc.relative_path)).unwrap(); + let actual = format_snapshot(&input, doc, &index.external_symbols); + let output_path = output_dir.join(&doc.relative_path); + if update { + fs_anyhow::create_dir_all(output_path.parent().unwrap()).unwrap(); + fs_anyhow::write(&output_path, actual).unwrap(); + continue; + } + let expected = fs_anyhow::read_to_string(&output_path).unwrap_or_else(|_| { + panic!( + "{} does not exist. Run with SCIP_UPDATE_SNAPSHOTS=1 to generate it.", + output_path.display() + ) + }); + pretty_assertions::assert_eq!( + expected, + actual, + "Snapshot mismatch for {}. Run with SCIP_UPDATE_SNAPSHOTS=1 to update.", + output_path.display() + ); + } + } + + #[test] + fn external_symbols_are_deduplicated() { + // `print` and `int` are each referenced more than once. + let symbols: Vec = index("class_nohint") + .external_symbols + .into_iter() + .map(|x| x.symbol) + .collect(); + assert_eq!( + symbols, + [ + "scip-python python python-stdlib 3.13 builtins/int#", + "scip-python python python-stdlib 3.13 builtins/print().", + ] + ); + } + + macro_rules! snapshot_tests { + ($($case:ident),* $(,)?) => { + $( + #[test] + fn $case() { + assert_snapshot(stringify!($case)); + } + )* + }; + } + + snapshot_tests!( + aliased_import, + builtin_imports, + class_nohint, + comprehensions, + dunder_vars, + empty_project_name_version, + enclosing_range_class, + enclosing_range_function, + f_string, + file_from_module_import, + graph_1278, + lambda_walrus, + nameparts_import, + nested_items, + nonlocal_skips_scope, + odd_pkg_name_1, + project_site_packages, + pyproject_1, + relative_import, + request_goofiness, + single_class, + single_function, + type_parameters, + unique, + unresolved_import, + ); +} diff --git a/pyrefly/lib/report/scip/snapshot.rs b/pyrefly/lib/report/scip/snapshot.rs new file mode 100644 index 0000000000..57975ba097 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshot.rs @@ -0,0 +1,373 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +//! A port of scip-python's `formatSnapshot` (`packages/pyright-scip/src/lib.ts`), so that +//! snapshots ported from scip-python keep the same layout. + +use std::collections::HashMap; +use std::collections::HashSet; + +use scip::types::Document; +use scip::types::Relationship; +use scip::types::SymbolInformation; +use scip::types::SymbolRole; + +use crate::report::scip::MANAGER; +use crate::report::scip::SCHEME; + +const FORMAT_OPTIONS_PREFIX: &str = "# format-options:"; + +#[derive(Default)] +struct FormatOptions { + show_docs: bool, + show_ranges: bool, +} + +impl FormatOptions { + fn parse(lines: &[&str]) -> Self { + let mut options = Self::default(); + if let Some(line) = lines + .iter() + .find_map(|line| line.strip_prefix(FORMAT_OPTIONS_PREFIX)) + { + for option in line.trim().split(',') { + match option.trim() { + "showDocs" => options.show_docs = true, + "showRanges" => options.show_ranges = true, + option => panic!("Invalid format option: {option}"), + } + } + } + options + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Position { + line: usize, + character: usize, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Range { + start: Position, + end: Position, +} + +impl Range { + fn from_scip(range: &[i32]) -> Self { + let (end_line, end_character) = if range.len() == 3 { + (range[0], range[2]) + } else { + (range[2], range[3]) + }; + Self { + start: Position { + line: range[0] as usize, + character: range[1] as usize, + }, + end: Position { + line: end_line as usize, + character: end_character as usize, + }, + } + } +} + +fn is_definition(roles: i32) -> bool { + roles & SymbolRole::Definition as i32 != 0 +} + +struct EnclosingRange<'a> { + range: Range, + symbol: &'a str, +} + +struct Formatter<'a> { + out: String, + options: FormatOptions, + symbols: HashMap<&'a str, &'a SymbolInformation>, + external_symbols: HashMap<&'a str, &'a SymbolInformation>, + symbols_with_definitions: HashSet<&'a str>, + emitted_docs: HashSet<&'a str>, +} + +impl<'a> Formatter<'a> { + fn push_doc(&mut self, range: Range, symbol: &'a str, definition: bool, start_of_line: bool) { + if self.emitted_docs.contains(symbol) + || (!definition && self.symbols_with_definitions.contains(symbol)) + { + self.out.push('\n'); + return; + } + self.emitted_docs.insert(symbol); + + let mut prefix = "\n#".to_owned(); + if !start_of_line { + prefix.push_str(&" ".repeat(range.start.character.saturating_sub(1))); + } + let (info, external) = match self.external_symbols.get(symbol) { + Some(info) => (Some(*info), true), + None => (self.symbols.get(symbol).copied(), false), + }; + if let Some(info) = info { + self.push_docs(&prefix, &info.documentation, external); + self.push_relationships(&prefix, &info.relationships); + } + self.out.push('\n'); + } + + fn push_docs(&mut self, prefix: &str, docs: &[String], external: bool) { + if !self.options.show_docs { + return; + } + for doc in docs { + for (i, line) in doc.split('\n').enumerate() { + self.out.push_str(prefix); + if i == 0 { + if external { + self.out.push_str("external "); + } + self.out.push_str("documentation "); + } else { + self.out.push_str(" > "); + } + // scip-python truncates by UTF-16 code unit; chars are close enough here. + let truncated: String = line.chars().take(40).collect(); + self.out.push_str(&truncated); + if line.chars().count() > 40 { + self.out.push_str("..."); + } + } + } + } + + fn push_relationships(&mut self, prefix: &str, relationships: &[Relationship]) { + let mut relationships: Vec<_> = relationships.iter().collect(); + relationships.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + for relationship in relationships { + self.out.push_str(prefix); + self.out.push_str("relationship"); + if relationship.is_implementation { + self.out.push_str(" implementation"); + } + if relationship.is_reference { + self.out.push_str(" reference"); + } + if relationship.is_type_definition { + self.out.push_str(" type_definition"); + } + self.out.push(' '); + self.out.push_str(&relationship.symbol); + } + } + + fn push_enclosing_range(&mut self, enclosing: &EnclosingRange, end: bool) { + let character = enclosing.range.start.character; + self.out.push('#'); + self.out + .push_str(&" ".repeat(character.saturating_sub(1).max(1))); + self.out.push(if character < 2 { + '<' + } else if end { + '^' + } else { + '⌄' + }); + self.out.push_str(if end { " end " } else { " start " }); + self.out.push_str("enclosing_range "); + self.out.push_str(enclosing.symbol); + self.out.push('\n'); + } +} + +pub fn format_snapshot( + input: &str, + doc: &Document, + external_symbols: &[SymbolInformation], +) -> String { + let lines: Vec<&str> = input.split('\n').collect(); + let mut formatter = Formatter { + out: String::new(), + options: FormatOptions::parse(&lines), + symbols: doc.symbols.iter().map(|s| (s.symbol.as_str(), s)).collect(), + external_symbols: external_symbols + .iter() + .map(|s| (s.symbol.as_str(), s)) + .collect(), + symbols_with_definitions: HashSet::new(), + emitted_docs: HashSet::new(), + }; + + let mut enclosing_ranges = Vec::new(); + for occurrence in &doc.occurrences { + if is_definition(occurrence.symbol_roles) { + formatter + .symbols_with_definitions + .insert(occurrence.symbol.as_str()); + } + if formatter.options.show_ranges && !occurrence.enclosing_range.is_empty() { + enclosing_ranges.push(EnclosingRange { + range: Range::from_scip(&occurrence.enclosing_range), + symbol: &occurrence.symbol, + }); + } + } + // The range that starts first, and for equal starts, the one that ends last. + enclosing_ranges.sort_by(|a, b| { + a.range + .cmp(&b.range) + .then(b.range.end.line.cmp(&a.range.end.line)) + }); + let mut starts: Vec> = (0..lines.len()).map(|_| Vec::new()).collect(); + let mut ends: Vec> = (0..lines.len()).map(|_| Vec::new()).collect(); + for enclosing in &enclosing_ranges { + starts[enclosing.range.start.line].push(enclosing); + ends[enclosing.range.end.line].insert(0, enclosing); + } + + let mut occurrences: Vec<_> = doc.occurrences.iter().collect(); + occurrences.sort_by_key(|o| Range::from_scip(&o.range)); + let package_prefix = format!("{SCHEME} {MANAGER}"); + let mut index = 0; + + for (line_number, line) in lines.iter().enumerate() { + // A zero-width occurrence at the start of the file is a file-level item, written above + // the first line. + if index == 0 + && let Some(occurrence) = occurrences.first() + { + let range = Range::from_scip(&occurrence.range); + if range.start.character == 0 && range.end.character == 0 { + let definition = is_definition(occurrence.symbol_roles); + formatter.out.push_str("# < "); + formatter.out.push_str(if definition { + "definition" + } else { + "reference" + }); + formatter.out.push(' '); + formatter.out.push_str(&occurrence.symbol); + formatter.push_doc(range, &occurrence.symbol, definition, true); + formatter.out.push('\n'); + index += 1; + } + } + + if formatter.options.show_ranges { + for enclosing in &starts[line_number] { + formatter.push_enclosing_range(enclosing, false); + } + } + + formatter.out.push_str(line); + formatter.out.push('\n'); + while let Some(occurrence) = occurrences.get(index) + && occurrence.range[0] as usize == line_number + { + index += 1; + assert!( + occurrence.range.len() == 3, + "multi-line occurrence ranges are not supported" + ); + let range = Range::from_scip(&occurrence.range); + let start_of_line = range.start.character == 0; + formatter.out.push('#'); + if !start_of_line { + formatter + .out + .push_str(&" ".repeat(range.start.character - 1)); + } + let caret_length = (range.end.character - range.start.character) + .checked_sub(start_of_line as usize) + .expect("negative length occurrence"); + formatter.out.push_str(&"^".repeat(caret_length)); + let definition = is_definition(occurrence.symbol_roles); + formatter.out.push_str(if definition { + " definition " + } else { + " reference " + }); + let symbol = occurrence + .symbol + .strip_prefix(&package_prefix) + .unwrap_or(&occurrence.symbol); + formatter.out.push_str(&symbol.replacen('\n', "|", 1)); + formatter.push_doc(range, &occurrence.symbol, definition, start_of_line); + } + + if formatter.options.show_ranges { + for enclosing in &ends[line_number] { + formatter.push_enclosing_range(enclosing, true); + } + } + } + formatter.out +} + +#[cfg(test)] +mod tests { + use scip::types::Occurrence; + + use super::*; + + fn occurrence(range: Vec, symbol: &str, roles: i32, enclosing: Vec) -> Occurrence { + Occurrence { + range, + symbol: symbol.to_owned(), + symbol_roles: roles, + enclosing_range: enclosing, + ..Default::default() + } + } + + #[test] + fn test_docs_relationships_and_ranges() { + let input = "# format-options: showDocs, showRanges\nclass C:\n def f(self):\n return self\n"; + let def = SymbolRole::Definition as i32; + let module = "scip-python python p 1 m/__init__:"; + let f = "scip-python python p 1 m/C#f()."; + let param = "scip-python python p 1 m/C#f().(self)"; + let doc = Document { + occurrences: vec![ + occurrence(vec![0, 0, 0], module, def, vec![]), + occurrence(vec![2, 8, 9], f, def, vec![2, 4, 3, 19]), + occurrence(vec![2, 10, 14], param, def, vec![]), + occurrence(vec![3, 15, 19], param, 0, vec![]), + ], + symbols: vec![SymbolInformation { + symbol: f.to_owned(), + documentation: vec!["```python\ndef f(self)\n```".to_owned()], + relationships: vec![Relationship { + symbol: "scip-python python p 1 m/B#f().".to_owned(), + is_implementation: true, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + let expected = r#"# < definition scip-python python p 1 m/__init__: + +# format-options: showDocs, showRanges +class C: +# ⌄ start enclosing_range scip-python python p 1 m/C#f(). + def f(self): +# ^ definition p 1 m/C#f(). +# documentation ```python +# > def f(self) +# > ``` +# relationship implementation scip-python python p 1 m/B#f(). +# ^^^^ definition p 1 m/C#f().(self) + return self +# ^^^^ reference p 1 m/C#f().(self) +# ^ end enclosing_range scip-python python p 1 m/C#f(). + +"#; + pretty_assertions::assert_eq!(expected, format_snapshot(input, &doc, &[])); + } +} diff --git a/pyrefly/lib/report/scip/snapshots/input/aliased_import/actual.py b/pyrefly/lib/report/scip/snapshots/input/aliased_import/actual.py new file mode 100644 index 0000000000..ad06034c82 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/aliased_import/actual.py @@ -0,0 +1,4 @@ +import aliased +import aliased as A + +print(A.SOME_CONSTANT) diff --git a/pyrefly/lib/report/scip/snapshots/input/aliased_import/aliased.py b/pyrefly/lib/report/scip/snapshots/input/aliased_import/aliased.py new file mode 100644 index 0000000000..0c0a66160a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/aliased_import/aliased.py @@ -0,0 +1 @@ +SOME_CONSTANT = "Some Constant" diff --git a/pyrefly/lib/report/scip/snapshots/input/builtin_imports/builtin_imports.py b/pyrefly/lib/report/scip/snapshots/input/builtin_imports/builtin_imports.py new file mode 100644 index 0000000000..b6bea9e794 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/builtin_imports/builtin_imports.py @@ -0,0 +1,6 @@ +# format-options: showDocs + +import re +from typing import Callable, Optional + +print(re, Callable, Optional) diff --git a/pyrefly/lib/report/scip/snapshots/input/class_nohint/class_nohint.py b/pyrefly/lib/report/scip/snapshots/input/class_nohint/class_nohint.py new file mode 100644 index 0000000000..8ae5ca8829 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/class_nohint/class_nohint.py @@ -0,0 +1,14 @@ +# format-options: showDocs + +class Example: + # Note, only y has a type hint + y: int + + def __init__(self, in_val): + self.x = in_val + self.x = self.x + 1 + self.y = in_val + + def something(self): + print(self.x) + print(self.y) diff --git a/pyrefly/lib/report/scip/snapshots/input/comprehensions/comp.py b/pyrefly/lib/report/scip/snapshots/input/comprehensions/comp.py new file mode 100644 index 0000000000..7d85f23a8b --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/comprehensions/comp.py @@ -0,0 +1,16 @@ +def something(x): + return 2 * x + +_ = [x for x in "should be local 0"] +_ = [something(x) for x in "should be local 1"] +_ = [something(x) for x in "should be local 2" if x == "s"] +_ = {k: x for (k, x) in enumerate(range(10))} + + +asdf = (var for var in [1, 2, 3] if var % 2 == 0) +for var in asdf: + print(var) + +# TODO: ListComprehensions with if +# TODO: Dictionary comprehensions + diff --git a/pyrefly/lib/report/scip/snapshots/input/dunder_vars/__main__.py b/pyrefly/lib/report/scip/snapshots/input/dunder_vars/__main__.py new file mode 100644 index 0000000000..5bd2e47d79 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/dunder_vars/__main__.py @@ -0,0 +1,4 @@ + +if __name__ == '__main__': + print("main") + diff --git a/pyrefly/lib/report/scip/snapshots/input/empty_project_name_version/main.py b/pyrefly/lib/report/scip/snapshots/input/empty_project_name_version/main.py new file mode 100644 index 0000000000..b4a3ff496a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/empty_project_name_version/main.py @@ -0,0 +1,4 @@ +def main(): + pass + +main() diff --git a/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/advanced.py b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/advanced.py new file mode 100644 index 0000000000..abdc18fcb1 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/advanced.py @@ -0,0 +1,13 @@ +# format-options: showRanges +def class_decorator(cls): + def wrapper(*args, **kwargs): + return cls(*args, **kwargs) + return wrapper + +@class_decorator +class Test: + def __init__(self, x: float): + self.x = x + + def test(self) -> float: + return self.x diff --git a/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/simple.py b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/simple.py new file mode 100644 index 0000000000..26746956e9 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_class/simple.py @@ -0,0 +1,3 @@ +# format-options: showRanges +class Test(): + pass diff --git a/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/advanced.py b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/advanced.py new file mode 100644 index 0000000000..5eff95e691 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/advanced.py @@ -0,0 +1,11 @@ +# format-options: showRanges +def decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +@decorator +def func(x: float) -> float: + test = x + + return test diff --git a/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/simple.py b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/simple.py new file mode 100644 index 0000000000..3bededc3bf --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/enclosing_range_function/simple.py @@ -0,0 +1,3 @@ +# format-options: showRanges +def simple(x): + return x diff --git a/pyrefly/lib/report/scip/snapshots/input/f_string/fstring.py b/pyrefly/lib/report/scip/snapshots/input/f_string/fstring.py new file mode 100644 index 0000000000..8726cb9159 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/f_string/fstring.py @@ -0,0 +1,3 @@ +var = ", world!" + +print(f"var: hello {var}") diff --git a/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/abc/file.py b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/abc/file.py new file mode 100644 index 0000000000..4d523c10eb --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/abc/file.py @@ -0,0 +1,3 @@ +from xyz import nested_file + +print(nested_file.X) diff --git a/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/file_from_module.py b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/file_from_module.py new file mode 100644 index 0000000000..4d523c10eb --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/file_from_module.py @@ -0,0 +1,3 @@ +from xyz import nested_file + +print(nested_file.X) diff --git a/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/xyz/__init__.py b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/xyz/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/xyz/nested_file.py b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/xyz/nested_file.py new file mode 100644 index 0000000000..5f4d995029 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/file_from_module_import/xyz/nested_file.py @@ -0,0 +1 @@ +X = "a constant" diff --git a/pyrefly/lib/report/scip/snapshots/input/graph_1278/mwe.py b/pyrefly/lib/report/scip/snapshots/input/graph_1278/mwe.py new file mode 100644 index 0000000000..bdafcec6bb --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/graph_1278/mwe.py @@ -0,0 +1,21 @@ +from typing import TypeVar, Generic, Callable, Iterator, ParamSpec + +_T_co = TypeVar("_T_co") +_P = ParamSpec("_P") + +class X(Generic[_T_co]): + pass + +def decorate(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, X[_T_co]]: ... + +class Foo: + @decorate + def foo(self) -> Iterator[None]: ... + +@decorate +def noop(): + yield + +class FooImpl(Foo): + def foo(self): + return noop() \ No newline at end of file diff --git a/pyrefly/lib/report/scip/snapshots/input/lambda_walrus/main.py b/pyrefly/lib/report/scip/snapshots/input/lambda_walrus/main.py new file mode 100644 index 0000000000..f0892200ef --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/lambda_walrus/main.py @@ -0,0 +1,3 @@ +# Each `hidden` should have a distinct local symbol. +first = lambda: (hidden := 1) +second = lambda: (hidden := 2) diff --git a/pyrefly/lib/report/scip/snapshots/input/nameparts_import/nameparts.py b/pyrefly/lib/report/scip/snapshots/input/nameparts_import/nameparts.py new file mode 100644 index 0000000000..ca52781502 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nameparts_import/nameparts.py @@ -0,0 +1,4 @@ +import importlib.resources + +importlib.resources.read_text('pre_commit.resources', 'filename') +importlib.resources.read_text('pre_commit.resources', 'filename') diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/__init__.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/__init__.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/__init__.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/__init__.py new file mode 100644 index 0000000000..174374a284 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/__init__.py @@ -0,0 +1,2 @@ +class InitClass: + init_item = 10 diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/baz/mod.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/baz/mod.py new file mode 100644 index 0000000000..86ea18d7c8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/foo/bar/baz/mod.py @@ -0,0 +1,5 @@ +class SuchNestedMuchWow: + class_item: int = 42 + +class AnotherNestedMuchWow: + other_item: int = 42 diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/importer.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/importer.py new file mode 100644 index 0000000000..c43986f8cd --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/importer.py @@ -0,0 +1,6 @@ +from foo.bar import InitClass +from foo.bar.baz.mod import SuchNestedMuchWow, AnotherNestedMuchWow + +print(SuchNestedMuchWow().class_item) +print(AnotherNestedMuchWow().other_item) +print(InitClass().init_item) diff --git a/pyrefly/lib/report/scip/snapshots/input/nested_items/src/long_importer.py b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/long_importer.py new file mode 100644 index 0000000000..bcfde5e76c --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nested_items/src/long_importer.py @@ -0,0 +1,3 @@ +import foo.bar.baz.mod + +print(foo.bar.baz.mod.SuchNestedMuchWow) diff --git a/pyrefly/lib/report/scip/snapshots/input/nonlocal_skips_scope/main.py b/pyrefly/lib/report/scip/snapshots/input/nonlocal_skips_scope/main.py new file mode 100644 index 0000000000..a3aebb3db3 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/nonlocal_skips_scope/main.py @@ -0,0 +1,13 @@ +# All occurrences of `x` should share the same symbol, skipping `middle`. +def outer(): + x = 0 + + def middle(): + def inner(): + nonlocal x + x = 1 + return x + + return inner + + return x, middle diff --git a/pyrefly/lib/report/scip/snapshots/input/odd_pkg_name_1/main.py b/pyrefly/lib/report/scip/snapshots/input/odd_pkg_name_1/main.py new file mode 100644 index 0000000000..1cfbad29fa --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/odd_pkg_name_1/main.py @@ -0,0 +1,4 @@ +def main(): + pass + +main() diff --git a/pyrefly/lib/report/scip/snapshots/input/project_site_packages/main.py b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/main.py new file mode 100644 index 0000000000..34ed8fce6b --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/main.py @@ -0,0 +1,4 @@ +# Its symbols should belong to review-dependency 2.0.0. +from review_dependency import Widget + +widget = Widget() diff --git a/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/METADATA b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/METADATA new file mode 100644 index 0000000000..c62baf0e1e --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: review-dependency +Version: 2.0.0 diff --git a/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/RECORD b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/RECORD new file mode 100644 index 0000000000..7a3edb34b2 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency-2.0.0.dist-info/RECORD @@ -0,0 +1,3 @@ +review_dependency/__init__.py,, +review_dependency-2.0.0.dist-info/METADATA,, +review_dependency-2.0.0.dist-info/RECORD,, diff --git a/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency/__init__.py b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency/__init__.py new file mode 100644 index 0000000000..915991d800 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/project_site_packages/site-packages/review_dependency/__init__.py @@ -0,0 +1,2 @@ +class Widget: + pass diff --git a/pyrefly/lib/report/scip/snapshots/input/pyproject_1/main.py b/pyrefly/lib/report/scip/snapshots/input/pyproject_1/main.py new file mode 100644 index 0000000000..1cfbad29fa --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/pyproject_1/main.py @@ -0,0 +1,4 @@ +def main(): + pass + +main() diff --git a/pyrefly/lib/report/scip/snapshots/input/pyproject_1/pyproject.toml b/pyrefly/lib/report/scip/snapshots/input/pyproject_1/pyproject.toml new file mode 100644 index 0000000000..3e51bbfbc6 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/pyproject_1/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "abc" + +[tool.poetry] +version = "16.05" diff --git a/pyrefly/lib/report/scip/snapshots/input/relative_import/bar.py b/pyrefly/lib/report/scip/snapshots/input/relative_import/bar.py new file mode 100644 index 0000000000..de5b84db49 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/relative_import/bar.py @@ -0,0 +1,7 @@ +from .foo import exported_function +from .foo import this_class + +if True: + exported_function() + + this_class.exported_function() diff --git a/pyrefly/lib/report/scip/snapshots/input/relative_import/foo.py b/pyrefly/lib/report/scip/snapshots/input/relative_import/foo.py new file mode 100644 index 0000000000..43398d1efe --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/relative_import/foo.py @@ -0,0 +1,13 @@ +def exported_function(): + return "function" + +class MyClass: + """This is a class and it is cool""" + + def __init__(self): + pass + + def exported_function(self): + return "exported" + +this_class = MyClass() diff --git a/pyrefly/lib/report/scip/snapshots/input/request_goofiness/goofy.py b/pyrefly/lib/report/scip/snapshots/input/request_goofiness/goofy.py new file mode 100644 index 0000000000..4862e66b1c --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/request_goofiness/goofy.py @@ -0,0 +1,3 @@ +import requests + +print(requests.get("https://sourcegraph.com")) diff --git a/pyrefly/lib/report/scip/snapshots/input/single_class/src/single_class.py b/pyrefly/lib/report/scip/snapshots/input/single_class/src/single_class.py new file mode 100644 index 0000000000..0646fcf957 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/single_class/src/single_class.py @@ -0,0 +1,13 @@ +class ExampleClass: + a: int + b: int + c: str + + static_var = "Hello World" + + def __init__(self, a: int, b: int): + local_c = ", world!" + + self.a = a + self.b = b + self.c = "hello" + local_c diff --git a/pyrefly/lib/report/scip/snapshots/input/single_function/src/single_function.py b/pyrefly/lib/report/scip/snapshots/input/single_function/src/single_function.py new file mode 100644 index 0000000000..5b2a5ef8e8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/single_function/src/single_function.py @@ -0,0 +1,13 @@ +def my_cool_function(a: str) -> str: + x = ", world" + return a + x + +def my_cool_function_2(a: str): + x = ", world" + return (lambda y: a + x + y)("oh no") + +def next_level(): + return my_cool_function + +my_cool_function("hello") +next_level()(a = "wow") diff --git a/pyrefly/lib/report/scip/snapshots/input/type_parameters/main.py b/pyrefly/lib/report/scip/snapshots/input/type_parameters/main.py new file mode 100644 index 0000000000..624e24a544 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/type_parameters/main.py @@ -0,0 +1,3 @@ +# The declaration of `T` and both annotations should share a defined symbol. +def identity[T](x: T) -> T: + return x diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/builtin_import_refs.py b/pyrefly/lib/report/scip/snapshots/input/unique/builtin_import_refs.py new file mode 100644 index 0000000000..df4abf22f8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/builtin_import_refs.py @@ -0,0 +1,3 @@ +from typing import Any + +print(Any) diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/field_docstring.py b/pyrefly/lib/report/scip/snapshots/input/unique/field_docstring.py new file mode 100644 index 0000000000..e7bd35227a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/field_docstring.py @@ -0,0 +1,6 @@ +# format-options: showDocs + +class ClassWithField: + + a: int + """Hello world, this is a""" diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/grandparent_impl.py b/pyrefly/lib/report/scip/snapshots/input/unique/grandparent_impl.py new file mode 100644 index 0000000000..0b0add32c8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/grandparent_impl.py @@ -0,0 +1,10 @@ +class A: + def grandparent(self) -> bool: + return True + +class B(A): + ... + +class C(B): + def grandparent(self) -> bool: + return False diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/inferred_field_docstring.py b/pyrefly/lib/report/scip/snapshots/input/unique/inferred_field_docstring.py new file mode 100644 index 0000000000..e1471fd570 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/inferred_field_docstring.py @@ -0,0 +1,5 @@ +# format-options: showDocs + +class ClassWithInferredField: + def __init__(self, b: int): + self.b = b diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/inherits_class.py b/pyrefly/lib/report/scip/snapshots/input/unique/inherits_class.py new file mode 100644 index 0000000000..0c27075555 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/inherits_class.py @@ -0,0 +1,16 @@ +class A: + def x(self) -> int: + raise NotImplemented + + def matched_despite_different_type(self, x: int): + pass + +class B(A): + def x(self) -> int: + return 5 + + def matched_despite_different_type(self, x: int, y: int): + pass + + def unrelated(self): + pass diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/module_docstring.py b/pyrefly/lib/report/scip/snapshots/input/unique/module_docstring.py new file mode 100644 index 0000000000..5d8a1ee205 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/module_docstring.py @@ -0,0 +1,8 @@ +# format-options: showDocs + +""" +This is a docstring for this module +""" + +def x(): + return 1 diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/multiinherits_test.py b/pyrefly/lib/report/scip/snapshots/input/unique/multiinherits_test.py new file mode 100644 index 0000000000..6eb4799ef5 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/multiinherits_test.py @@ -0,0 +1,26 @@ +class Left: + def one(self) -> int: + return 1 + + def shared(self) -> bool: + return False + +class Right: + def two(self): + return 2 + + def shared(self) -> bool: + return False + +class Multi(Left, Right): + def one(self) -> int: + return 1 + + def two(self): + return 2 + + def three(self): + return 3 + + def shared(self) -> bool: + return True diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/property_access.py b/pyrefly/lib/report/scip/snapshots/input/unique/property_access.py new file mode 100644 index 0000000000..d883c5b587 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/property_access.py @@ -0,0 +1,17 @@ +from typing import Sequence + +class PropertyClass: + def __init__(self): + pass + + @property + def prop_ref(self): + return 5 + + +xs = [PropertyClass()] + +def usage(xs: Sequence[PropertyClass]): + def nested(): + for x in xs: + print(x.prop_ref) diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/rebinding.py b/pyrefly/lib/report/scip/snapshots/input/unique/rebinding.py new file mode 100644 index 0000000000..a90daa4ce0 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/rebinding.py @@ -0,0 +1,41 @@ +# Only the first binding of a symbol is its definition, except that every overload defines its +# parameters. +from typing import overload + +x = 1 +x = 2 + + +def f(): + y = 1 + y = 2 + return y + + +class C: + a = 1 + a = 2 + + def __init__(self): + self.b = 1 + self.b = 2 + + @overload + def m(self, v: int) -> int: ... + @overload + def m(self, v: str) -> str: ... + def m(self, v): + return v + + +def g(): ... +def g(): ... + + +def h(p): + p = 1 + return p + + +for x in range(3): + pass diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/stdlib_backport.py b/pyrefly/lib/report/scip/snapshots/input/unique/stdlib_backport.py new file mode 100644 index 0000000000..17f5ee8de1 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/stdlib_backport.py @@ -0,0 +1,5 @@ +# typing_extensions is in the bundled stdlib stubs, but an installed distribution takes +# precedence. +from typing_extensions import Doc + +print(Doc) diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/vars_inside_scopes.py b/pyrefly/lib/report/scip/snapshots/input/unique/vars_inside_scopes.py new file mode 100644 index 0000000000..418b7318d8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/vars_inside_scopes.py @@ -0,0 +1,11 @@ +from typing import List + +class X: + items: List[int] + + def my_func(self): + for x in self.items: + y = x + 1 + + if 5 in self.items: + z = "oh ya" diff --git a/pyrefly/lib/report/scip/snapshots/input/unique/walrus.py b/pyrefly/lib/report/scip/snapshots/input/unique/walrus.py new file mode 100644 index 0000000000..b21d31c65d --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unique/walrus.py @@ -0,0 +1,27 @@ +import re + +some_list = [1, 2, 3, 4, 5, 100, 1000] +pattern = "^[A-Za-z0-9_]*$" +text = "Some_name123" + +# if statement +if (n := len(some_list)) > 10: + print(f"List is too long with {n} elements.") + +if (match := re.search(pattern, text)) is not None: + print("Found match:", match.group(0)) + + +# comprehensions +def show_some_comprehension(): + [print(x, root) for x in some_list if (x % 2 == 0) and (root := x**0.5) > 5] + + +# while loop +while (line := input("Enter text: ")) != "quit": + print(f"You entered: {line}") + + +# if + comprehension +if any((any_n := num) < 0 for num in some_list): + print(f"Negative number found: {any_n}") diff --git a/pyrefly/lib/report/scip/snapshots/input/unresolved_import/unresolved.py b/pyrefly/lib/report/scip/snapshots/input/unresolved_import/unresolved.py new file mode 100644 index 0000000000..4747f7b96d --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/input/unresolved_import/unresolved.py @@ -0,0 +1,5 @@ +import this_is_not_real + +print(this_is_not_real.x) +print(this_is_not_real.x) + diff --git a/pyrefly/lib/report/scip/snapshots/output/aliased_import/actual.py b/pyrefly/lib/report/scip/snapshots/output/aliased_import/actual.py new file mode 100644 index 0000000000..5eb646a533 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/aliased_import/actual.py @@ -0,0 +1,13 @@ +# < definition scip-python python snapshot-util 0.1 actual/__init__: + +import aliased +# ^^^^^^^ reference snapshot-util 0.1 aliased/__init__: +import aliased as A +# ^^^^^^^ reference snapshot-util 0.1 aliased/__init__: +# ^ reference snapshot-util 0.1 aliased/__init__: + +print(A.SOME_CONSTANT) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^ reference snapshot-util 0.1 aliased/__init__: +# ^^^^^^^^^^^^^ reference snapshot-util 0.1 aliased/SOME_CONSTANT. + diff --git a/pyrefly/lib/report/scip/snapshots/output/aliased_import/aliased.py b/pyrefly/lib/report/scip/snapshots/output/aliased_import/aliased.py new file mode 100644 index 0000000000..4335bc015f --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/aliased_import/aliased.py @@ -0,0 +1,5 @@ +# < definition scip-python python snapshot-util 0.1 aliased/__init__: + +SOME_CONSTANT = "Some Constant" +#^^^^^^^^^^^^ definition snapshot-util 0.1 aliased/SOME_CONSTANT. + diff --git a/pyrefly/lib/report/scip/snapshots/output/builtin_imports/builtin_imports.py b/pyrefly/lib/report/scip/snapshots/output/builtin_imports/builtin_imports.py new file mode 100644 index 0000000000..62010e5c43 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/builtin_imports/builtin_imports.py @@ -0,0 +1,53 @@ +# < definition scip-python python snapshot-util 0.1 builtin_imports/__init__: +#documentation ```python +# > (module) builtin_imports +# > ``` + +# format-options: showDocs + +import re +# ^^ reference python-stdlib 3.13 re/__init__: +# external documentation ```python +# > (module) re: Module[re] +# > ``` +from typing import Callable, Optional +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# external documentation ```python +# > (module) typing: Module[typing] +# > ``` +# ^^^^^^^^ reference python-stdlib 3.13 typing/Callable. +# external documentation ```python +# > (class) Callable: type[Callable] +# > ``` +# ^^^^^^^^ reference python-stdlib 3.13 typing/Optional. +# external documentation ```python +# > (class) Optional: type[Optional] +# > ``` + +print(re, Callable, Optional) +#^^^^ reference python-stdlib 3.13 builtins/print(). +#external documentation ```python +# > (function) print: def print( +# > *values: object, +# > *, +# > sep : str | None =... +# > end : str | None =... +# > file : SupportsWrite[str] | None =... +# > flush : Literal[False] =... +# > ) -> None: ... +# > ``` +# > --- +# > Prints the values to a stream, or to sys... +# > +# > sep +# >   string inserted between valu... +# > end +# >   string appended after the la... +# > file +# >   a file-like object (stream);... +# > flush +# >   whether to forcibly flush th... +# ^^ reference python-stdlib 3.13 re/__init__: +# ^^^^^^^^ reference python-stdlib 3.13 typing/Callable. +# ^^^^^^^^ reference python-stdlib 3.13 typing/Optional. + diff --git a/pyrefly/lib/report/scip/snapshots/output/class_nohint/class_nohint.py b/pyrefly/lib/report/scip/snapshots/output/class_nohint/class_nohint.py new file mode 100644 index 0000000000..e9570af43d --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/class_nohint/class_nohint.py @@ -0,0 +1,119 @@ +# < definition scip-python python snapshot-util 0.1 class_nohint/__init__: +#documentation ```python +# > (module) class_nohint +# > ``` + +# format-options: showDocs + +class Example: +# ^^^^^^^ definition snapshot-util 0.1 class_nohint/Example# +# documentation ```python +# > (class) Example: type[Example] +# > ``` + # Note, only y has a type hint + y: int +# ^ definition snapshot-util 0.1 class_nohint/Example#y. +# documentation ```python +# > (variable) y: int +# > ``` +# ^^^ reference python-stdlib 3.13 builtins/int# +# external documentation ```python +# > (class) int: +# > @overload +# > def __new__(x: ConvertibleToInt = 0, /) ... +# > def __new__( +# > x: bytearray | bytes | str, +# > /, +# > base: SupportsIndex +# > ) -> int: ... +# > ``` +# > --- +# > int([x]) -> integer +# > int(x, base=10) -> integer +# > +# > Convert a number or string to an integer... +# > are given. If x is a number, return x._... +# > numbers, this truncates towards zero. +# > +# > If x is not a number or if base is given... +# > bytes, or bytearray instance representin... +# > given base. The literal can be preceded... +# > by whitespace. The base defaults to 10.... +# > Base 0 means to interpret the base from ... +# > ```python +# > >>> int('0b100', base=0) +# > ``` +# > 4 + + def __init__(self, in_val): +# ^^^^^^^^ definition snapshot-util 0.1 class_nohint/Example#__init__(). +# documentation ```python +# > (method) __init__: def __init__( +# > self: Self@Example, +# > in_val: Unknown +# > ) -> None: ... +# > ``` +# ^^^^ definition snapshot-util 0.1 class_nohint/Example#__init__().(self) +# documentation ```python +# > (parameter) self: Self@Example +# > ``` +# ^^^^^^ definition snapshot-util 0.1 class_nohint/Example#__init__().(in_val) +# documentation ```python +# > (parameter) in_val: Unknown +# > ``` + self.x = in_val +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(self) +# ^ definition snapshot-util 0.1 class_nohint/Example#x. +# documentation ```python +# > (attribute) x: Unknown +# > ``` +# ^^^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(in_val) + self.x = self.x + 1 +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(self) +# ^ reference snapshot-util 0.1 class_nohint/Example#x. +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(self) +# ^ reference snapshot-util 0.1 class_nohint/Example#x. + self.y = in_val +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(self) +# ^ reference snapshot-util 0.1 class_nohint/Example#y. +# ^^^^^^ reference snapshot-util 0.1 class_nohint/Example#__init__().(in_val) + + def something(self): +# ^^^^^^^^^ definition snapshot-util 0.1 class_nohint/Example#something(). +# documentation ```python +# > (method) something: def something(self: ... +# > ``` +# ^^^^ definition snapshot-util 0.1 class_nohint/Example#something().(self) +# documentation ```python +# > (parameter) self: Self@Example +# > ``` + print(self.x) +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# external documentation ```python +# > (function) print: def print( +# > *values: object, +# > *, +# > sep : str | None =... +# > end : str | None =... +# > file : SupportsWrite[str] | None =... +# > flush : Literal[False] =... +# > ) -> None: ... +# > ``` +# > --- +# > Prints the values to a stream, or to sys... +# > +# > sep +# >   string inserted between valu... +# > end +# >   string appended after the la... +# > file +# >   a file-like object (stream);... +# > flush +# >   whether to forcibly flush th... +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#something().(self) +# ^ reference snapshot-util 0.1 class_nohint/Example#x. + print(self.y) +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^ reference snapshot-util 0.1 class_nohint/Example#something().(self) +# ^ reference snapshot-util 0.1 class_nohint/Example#y. + diff --git a/pyrefly/lib/report/scip/snapshots/output/comprehensions/comp.py b/pyrefly/lib/report/scip/snapshots/output/comprehensions/comp.py new file mode 100644 index 0000000000..a70c6ed30c --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/comprehensions/comp.py @@ -0,0 +1,49 @@ +# < definition scip-python python snapshot-util 0.1 comp/__init__: + +def something(x): +# ^^^^^^^^^ definition snapshot-util 0.1 comp/something(). +# ^ definition snapshot-util 0.1 comp/something().(x) + return 2 * x +# ^ reference snapshot-util 0.1 comp/something().(x) + +_ = [x for x in "should be local 0"] +# definition snapshot-util 0.1 comp/_. +# ^ reference local 0 +# ^ definition local 0 +_ = [something(x) for x in "should be local 1"] +# reference snapshot-util 0.1 comp/_. +# ^^^^^^^^^ reference snapshot-util 0.1 comp/something(). +# ^ reference local 1 +# ^ definition local 1 +_ = [something(x) for x in "should be local 2" if x == "s"] +# reference snapshot-util 0.1 comp/_. +# ^^^^^^^^^ reference snapshot-util 0.1 comp/something(). +# ^ reference local 2 +# ^ definition local 2 +# ^ reference local 2 +_ = {k: x for (k, x) in enumerate(range(10))} +# reference snapshot-util 0.1 comp/_. +# ^ reference local 3 +# ^ reference local 4 +# ^ definition local 3 +# ^ definition local 4 +# ^^^^^^^^^ reference python-stdlib 3.13 builtins/enumerate# +# ^^^^^ reference python-stdlib 3.13 builtins/range# + + +asdf = (var for var in [1, 2, 3] if var % 2 == 0) +#^^^ definition snapshot-util 0.1 comp/asdf. +# ^^^ reference local 5 +# ^^^ definition local 5 +# ^^^ reference local 5 +for var in asdf: +# ^^^ definition snapshot-util 0.1 comp/var. +# ^^^^ reference snapshot-util 0.1 comp/asdf. + print(var) +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^ reference snapshot-util 0.1 comp/var. + +# TODO: ListComprehensions with if +# TODO: Dictionary comprehensions + + diff --git a/pyrefly/lib/report/scip/snapshots/output/dunder_vars/__main__.py b/pyrefly/lib/report/scip/snapshots/output/dunder_vars/__main__.py new file mode 100644 index 0000000000..9d1b2104c6 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/dunder_vars/__main__.py @@ -0,0 +1,9 @@ +# < definition scip-python python snapshot-util 0.1 __main__/__init__: + + +if __name__ == '__main__': +# ^^^^^^^^ reference python-stdlib 3.13 builtins/__name__# + print("main") +# ^^^^^ reference python-stdlib 3.13 builtins/print(). + + diff --git a/pyrefly/lib/report/scip/snapshots/output/empty_project_name_version/main.py b/pyrefly/lib/report/scip/snapshots/output/empty_project_name_version/main.py new file mode 100644 index 0000000000..a6e76749f0 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/empty_project_name_version/main.py @@ -0,0 +1,9 @@ +# < definition scip-python python . . main/__init__: + +def main(): +# ^^^^ definition . . main/main(). + pass + +main() +#^^^ reference . . main/main(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/advanced.py b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/advanced.py new file mode 100644 index 0000000000..b99e3dc076 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/advanced.py @@ -0,0 +1,49 @@ +# < definition scip-python python snapshot-util 0.1 advanced/__init__: + +# format-options: showRanges +# < start enclosing_range scip-python python snapshot-util 0.1 advanced/class_decorator(). +def class_decorator(cls): +# ^^^^^^^^^^^^^^^ definition snapshot-util 0.1 advanced/class_decorator(). +# ^^^ definition snapshot-util 0.1 advanced/class_decorator().(cls) +# ⌄ start enclosing_range scip-python python snapshot-util 0.1 advanced/class_decorator().wrapper(). + def wrapper(*args, **kwargs): +# ^^^^^^^ definition snapshot-util 0.1 advanced/class_decorator().wrapper(). +# ^^^^ definition snapshot-util 0.1 advanced/class_decorator().wrapper().(args) +# ^^^^^^ definition snapshot-util 0.1 advanced/class_decorator().wrapper().(kwargs) + return cls(*args, **kwargs) +# ^^^ reference snapshot-util 0.1 advanced/class_decorator().(cls) +# ^^^^ reference snapshot-util 0.1 advanced/class_decorator().wrapper().(args) +# ^^^^^^ reference snapshot-util 0.1 advanced/class_decorator().wrapper().(kwargs) +# ^ end enclosing_range scip-python python snapshot-util 0.1 advanced/class_decorator().wrapper(). + return wrapper +# ^^^^^^^ reference snapshot-util 0.1 advanced/class_decorator().wrapper(). +# < end enclosing_range scip-python python snapshot-util 0.1 advanced/class_decorator(). + +# < start enclosing_range scip-python python snapshot-util 0.1 advanced/Test# +@class_decorator +#^^^^^^^^^^^^^^^ reference snapshot-util 0.1 advanced/class_decorator(). +class Test: +# ^^^^ definition snapshot-util 0.1 advanced/Test# +# ⌄ start enclosing_range scip-python python snapshot-util 0.1 advanced/Test#__init__(). + def __init__(self, x: float): +# ^^^^^^^^ definition snapshot-util 0.1 advanced/Test#__init__(). +# ^^^^ definition snapshot-util 0.1 advanced/Test#__init__().(self) +# ^ definition snapshot-util 0.1 advanced/Test#__init__().(x) +# ^^^^^ reference python-stdlib 3.13 builtins/float# + self.x = x +# ^^^^ reference snapshot-util 0.1 advanced/Test#__init__().(self) +# ^ definition snapshot-util 0.1 advanced/Test#x. +# ^ reference snapshot-util 0.1 advanced/Test#__init__().(x) +# ^ end enclosing_range scip-python python snapshot-util 0.1 advanced/Test#__init__(). + +# ⌄ start enclosing_range scip-python python snapshot-util 0.1 advanced/Test#test(). + def test(self) -> float: +# ^^^^ definition snapshot-util 0.1 advanced/Test#test(). +# ^^^^ definition snapshot-util 0.1 advanced/Test#test().(self) +# ^^^^^ reference python-stdlib 3.13 builtins/float# + return self.x +# ^^^^ reference snapshot-util 0.1 advanced/Test#test().(self) +# ^ reference snapshot-util 0.1 advanced/Test#x. +# ^ end enclosing_range scip-python python snapshot-util 0.1 advanced/Test#test(). +# < end enclosing_range scip-python python snapshot-util 0.1 advanced/Test# + diff --git a/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/simple.py b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/simple.py new file mode 100644 index 0000000000..71a71c002a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_class/simple.py @@ -0,0 +1,9 @@ +# < definition scip-python python snapshot-util 0.1 simple/__init__: + +# format-options: showRanges +# < start enclosing_range scip-python python snapshot-util 0.1 simple/Test# +class Test(): +# ^^^^ definition snapshot-util 0.1 simple/Test# + pass +# < end enclosing_range scip-python python snapshot-util 0.1 simple/Test# + diff --git a/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/advanced.py b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/advanced.py new file mode 100644 index 0000000000..74f40c6e35 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/advanced.py @@ -0,0 +1,37 @@ +# < definition scip-python python snapshot-util 0.1 advanced/__init__: + +# format-options: showRanges +# < start enclosing_range scip-python python snapshot-util 0.1 advanced/decorator(). +def decorator(func): +# ^^^^^^^^^ definition snapshot-util 0.1 advanced/decorator(). +# ^^^^ definition snapshot-util 0.1 advanced/decorator().(func) +# ⌄ start enclosing_range scip-python python snapshot-util 0.1 advanced/decorator().wrapper(). + def wrapper(*args, **kwargs): +# ^^^^^^^ definition snapshot-util 0.1 advanced/decorator().wrapper(). +# ^^^^ definition snapshot-util 0.1 advanced/decorator().wrapper().(args) +# ^^^^^^ definition snapshot-util 0.1 advanced/decorator().wrapper().(kwargs) + return func(*args, **kwargs) +# ^^^^ reference snapshot-util 0.1 advanced/decorator().(func) +# ^^^^ reference snapshot-util 0.1 advanced/decorator().wrapper().(args) +# ^^^^^^ reference snapshot-util 0.1 advanced/decorator().wrapper().(kwargs) +# ^ end enclosing_range scip-python python snapshot-util 0.1 advanced/decorator().wrapper(). + return wrapper +# ^^^^^^^ reference snapshot-util 0.1 advanced/decorator().wrapper(). +# < end enclosing_range scip-python python snapshot-util 0.1 advanced/decorator(). + +# < start enclosing_range scip-python python snapshot-util 0.1 advanced/func(). +@decorator +#^^^^^^^^^ reference snapshot-util 0.1 advanced/decorator(). +def func(x: float) -> float: +# ^^^^ definition snapshot-util 0.1 advanced/func(). +# ^ definition snapshot-util 0.1 advanced/func().(x) +# ^^^^^ reference python-stdlib 3.13 builtins/float# +# ^^^^^ reference python-stdlib 3.13 builtins/float# + test = x +# ^^^^ definition local 0 +# ^ reference snapshot-util 0.1 advanced/func().(x) + + return test +# ^^^^ reference local 0 +# < end enclosing_range scip-python python snapshot-util 0.1 advanced/func(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/simple.py b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/simple.py new file mode 100644 index 0000000000..fe80f65e24 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/enclosing_range_function/simple.py @@ -0,0 +1,11 @@ +# < definition scip-python python snapshot-util 0.1 simple/__init__: + +# format-options: showRanges +# < start enclosing_range scip-python python snapshot-util 0.1 simple/simple(). +def simple(x): +# ^^^^^^ definition snapshot-util 0.1 simple/simple(). +# ^ definition snapshot-util 0.1 simple/simple().(x) + return x +# ^ reference snapshot-util 0.1 simple/simple().(x) +# < end enclosing_range scip-python python snapshot-util 0.1 simple/simple(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/f_string/fstring.py b/pyrefly/lib/report/scip/snapshots/output/f_string/fstring.py new file mode 100644 index 0000000000..def92e6b15 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/f_string/fstring.py @@ -0,0 +1,9 @@ +# < definition scip-python python snapshot-util 0.1 fstring/__init__: + +var = ", world!" +#^^ definition snapshot-util 0.1 fstring/var. + +print(f"var: hello {var}") +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^ reference snapshot-util 0.1 fstring/var. + diff --git a/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/abc/file.py b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/abc/file.py new file mode 100644 index 0000000000..a3106d0b5f --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/abc/file.py @@ -0,0 +1,11 @@ +# < definition scip-python python snapshot-util 0.1 `abc.file`/__init__: + +from xyz import nested_file +# ^^^ reference snapshot-util 0.1 xyz/__init__: +# ^^^^^^^^^^^ reference snapshot-util 0.1 `xyz.nested_file`/__init__: + +print(nested_file.X) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^ reference snapshot-util 0.1 `xyz.nested_file`/__init__: +# ^ reference snapshot-util 0.1 `xyz.nested_file`/X. + diff --git a/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/file_from_module.py b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/file_from_module.py new file mode 100644 index 0000000000..29387cbf9c --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/file_from_module.py @@ -0,0 +1,11 @@ +# < definition scip-python python snapshot-util 0.1 file_from_module/__init__: + +from xyz import nested_file +# ^^^ reference snapshot-util 0.1 xyz/__init__: +# ^^^^^^^^^^^ reference snapshot-util 0.1 `xyz.nested_file`/__init__: + +print(nested_file.X) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^ reference snapshot-util 0.1 `xyz.nested_file`/__init__: +# ^ reference snapshot-util 0.1 `xyz.nested_file`/X. + diff --git a/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/__init__.py b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/__init__.py new file mode 100644 index 0000000000..25308027e7 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/__init__.py @@ -0,0 +1,3 @@ +# < definition scip-python python snapshot-util 0.1 xyz/__init__: + + diff --git a/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/nested_file.py b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/nested_file.py new file mode 100644 index 0000000000..62f17e5b7f --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/file_from_module_import/xyz/nested_file.py @@ -0,0 +1,5 @@ +# < definition scip-python python snapshot-util 0.1 `xyz.nested_file`/__init__: + +X = "a constant" +# definition snapshot-util 0.1 `xyz.nested_file`/X. + diff --git a/pyrefly/lib/report/scip/snapshots/output/graph_1278/mwe.py b/pyrefly/lib/report/scip/snapshots/output/graph_1278/mwe.py new file mode 100644 index 0000000000..b7c9b702c2 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/graph_1278/mwe.py @@ -0,0 +1,61 @@ +# < definition scip-python python snapshot-util 0.1 mwe/__init__: + +from typing import TypeVar, Generic, Callable, Iterator, ParamSpec +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# ^^^^^^^ reference python-stdlib 3.13 typing/TypeVar# +# ^^^^^^^ reference python-stdlib 3.13 typing/Generic. +# ^^^^^^^^ reference python-stdlib 3.13 typing/Callable. +# ^^^^^^^^ reference python-stdlib 3.13 typing/Iterator# +# ^^^^^^^^^ reference python-stdlib 3.13 typing/ParamSpec# + +_T_co = TypeVar("_T_co") +#^^^^ definition snapshot-util 0.1 mwe/_T_co. +# ^^^^^^^ reference python-stdlib 3.13 typing/TypeVar# +_P = ParamSpec("_P") +#^ definition snapshot-util 0.1 mwe/_P. +# ^^^^^^^^^ reference python-stdlib 3.13 typing/ParamSpec# + +class X(Generic[_T_co]): +# ^ definition snapshot-util 0.1 mwe/X# +# relationship implementation scip-python python python-stdlib 3.13 typing/Generic. +# ^^^^^^^ reference python-stdlib 3.13 typing/Generic. +# ^^^^^ reference snapshot-util 0.1 mwe/_T_co. + pass + +def decorate(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, X[_T_co]]: ... +# ^^^^^^^^ definition snapshot-util 0.1 mwe/decorate(). +# ^^^^ definition snapshot-util 0.1 mwe/decorate().(func) +# ^^^^^^^^ reference python-stdlib 3.13 typing/Callable. +# ^^ reference snapshot-util 0.1 mwe/_P. +# ^^^^^^^^ reference python-stdlib 3.13 typing/Iterator# +# ^^^^^ reference snapshot-util 0.1 mwe/_T_co. +# ^^^^^^^^ reference python-stdlib 3.13 typing/Callable. +# ^^ reference snapshot-util 0.1 mwe/_P. +# ^ reference snapshot-util 0.1 mwe/X# +# ^^^^^ reference snapshot-util 0.1 mwe/_T_co. + +class Foo: +# ^^^ definition snapshot-util 0.1 mwe/Foo# + @decorate +# ^^^^^^^^ reference snapshot-util 0.1 mwe/decorate(). + def foo(self) -> Iterator[None]: ... +# ^^^ definition snapshot-util 0.1 mwe/Foo#foo(). +# ^^^^ definition snapshot-util 0.1 mwe/Foo#foo().(self) +# ^^^^^^^^ reference python-stdlib 3.13 typing/Iterator# + +@decorate +#^^^^^^^^ reference snapshot-util 0.1 mwe/decorate(). +def noop(): +# ^^^^ definition snapshot-util 0.1 mwe/noop(). + yield + +class FooImpl(Foo): +# ^^^^^^^ definition snapshot-util 0.1 mwe/FooImpl# +# relationship implementation scip-python python snapshot-util 0.1 mwe/Foo# +# ^^^ reference snapshot-util 0.1 mwe/Foo# + def foo(self): +# ^^^ definition snapshot-util 0.1 mwe/FooImpl#foo(). +# relationship implementation scip-python python snapshot-util 0.1 mwe/Foo#foo(). +# ^^^^ definition snapshot-util 0.1 mwe/FooImpl#foo().(self) + return noop() +# ^^^^ reference snapshot-util 0.1 mwe/noop(). diff --git a/pyrefly/lib/report/scip/snapshots/output/lambda_walrus/main.py b/pyrefly/lib/report/scip/snapshots/output/lambda_walrus/main.py new file mode 100644 index 0000000000..75445d76e7 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/lambda_walrus/main.py @@ -0,0 +1,10 @@ +# < definition scip-python python snapshot-util 0.1 main/__init__: + +# Each `hidden` should have a distinct local symbol. +first = lambda: (hidden := 1) +#^^^^ definition snapshot-util 0.1 main/first. +# ^^^^^^ definition local 0 +second = lambda: (hidden := 2) +#^^^^^ definition snapshot-util 0.1 main/second. +# ^^^^^^ definition local 1 + diff --git a/pyrefly/lib/report/scip/snapshots/output/nameparts_import/nameparts.py b/pyrefly/lib/report/scip/snapshots/output/nameparts_import/nameparts.py new file mode 100644 index 0000000000..aa38c26333 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nameparts_import/nameparts.py @@ -0,0 +1,14 @@ +# < definition scip-python python snapshot-util 0.1 nameparts/__init__: + +import importlib.resources +# ^^^^^^^^^^^^^^^^^^^ reference python-stdlib 3.13 `importlib.resources`/__init__: + +importlib.resources.read_text('pre_commit.resources', 'filename') +#^^^^^^^^ reference python-stdlib 3.13 importlib/__init__: +# ^^^^^^^^^ reference python-stdlib 3.13 `importlib.resources`/__init__: +# ^^^^^^^^^ reference python-stdlib 3.13 `importlib.resources._functional`/read_text(). +importlib.resources.read_text('pre_commit.resources', 'filename') +#^^^^^^^^ reference python-stdlib 3.13 importlib/__init__: +# ^^^^^^^^^ reference python-stdlib 3.13 `importlib.resources`/__init__: +# ^^^^^^^^^ reference python-stdlib 3.13 `importlib.resources._functional`/read_text(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/__init__.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/__init__.py new file mode 100644 index 0000000000..5da15c4a1e --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/__init__.py @@ -0,0 +1,3 @@ +# < definition scip-python python snapshot-util 0.1 /__init__: + + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/__init__.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/__init__.py new file mode 100644 index 0000000000..1bbbbacc1a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/__init__.py @@ -0,0 +1,3 @@ +# < definition scip-python python snapshot-util 0.1 foo/__init__: + + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/__init__.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/__init__.py new file mode 100644 index 0000000000..7bac12003b --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/__init__.py @@ -0,0 +1,7 @@ +# < definition scip-python python snapshot-util 0.1 `foo.bar`/__init__: + +class InitClass: +# ^^^^^^^^^ definition snapshot-util 0.1 `foo.bar`/InitClass# + init_item = 10 +# ^^^^^^^^^ definition snapshot-util 0.1 `foo.bar`/InitClass#init_item. + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/baz/mod.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/baz/mod.py new file mode 100644 index 0000000000..713670fbeb --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/foo/bar/baz/mod.py @@ -0,0 +1,14 @@ +# < definition scip-python python snapshot-util 0.1 `foo.bar.baz.mod`/__init__: + +class SuchNestedMuchWow: +# ^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow# + class_item: int = 42 +# ^^^^^^^^^^ definition snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow#class_item. +# ^^^ reference python-stdlib 3.13 builtins/int# + +class AnotherNestedMuchWow: +# ^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 `foo.bar.baz.mod`/AnotherNestedMuchWow# + other_item: int = 42 +# ^^^^^^^^^^ definition snapshot-util 0.1 `foo.bar.baz.mod`/AnotherNestedMuchWow#other_item. +# ^^^ reference python-stdlib 3.13 builtins/int# + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/importer.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/importer.py new file mode 100644 index 0000000000..165962035b --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/importer.py @@ -0,0 +1,23 @@ +# < definition scip-python python snapshot-util 0.1 importer/__init__: + +from foo.bar import InitClass +# ^^^^^^^ reference snapshot-util 0.1 `foo.bar`/__init__: +# ^^^^^^^^^ reference snapshot-util 0.1 `foo.bar`/InitClass# +from foo.bar.baz.mod import SuchNestedMuchWow, AnotherNestedMuchWow +# ^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/__init__: +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow# +# ^^^^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/AnotherNestedMuchWow# + +print(SuchNestedMuchWow().class_item) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow# +# ^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow#class_item. +print(AnotherNestedMuchWow().other_item) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/AnotherNestedMuchWow# +# ^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/AnotherNestedMuchWow#other_item. +print(InitClass().init_item) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^ reference snapshot-util 0.1 `foo.bar`/InitClass# +# ^^^^^^^^^ reference snapshot-util 0.1 `foo.bar`/InitClass#init_item. + diff --git a/pyrefly/lib/report/scip/snapshots/output/nested_items/src/long_importer.py b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/long_importer.py new file mode 100644 index 0000000000..fc7411bf8d --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nested_items/src/long_importer.py @@ -0,0 +1,12 @@ +# < definition scip-python python snapshot-util 0.1 long_importer/__init__: + +import foo.bar.baz.mod +# ^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/__init__: + +print(foo.bar.baz.mod.SuchNestedMuchWow) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^ reference snapshot-util 0.1 foo/__init__: +# ^^^ reference snapshot-util 0.1 `foo.bar`/__init__: +# ^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/__init__: +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 `foo.bar.baz.mod`/SuchNestedMuchWow# + diff --git a/pyrefly/lib/report/scip/snapshots/output/nonlocal_skips_scope/main.py b/pyrefly/lib/report/scip/snapshots/output/nonlocal_skips_scope/main.py new file mode 100644 index 0000000000..e5ec1d8c5b --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/nonlocal_skips_scope/main.py @@ -0,0 +1,25 @@ +# < definition scip-python python snapshot-util 0.1 main/__init__: + +# All occurrences of `x` should share the same symbol, skipping `middle`. +def outer(): +# ^^^^^ definition snapshot-util 0.1 main/outer(). + x = 0 +# ^ definition local 0 + + def middle(): +# ^^^^^^ definition snapshot-util 0.1 main/outer().middle(). + def inner(): +# ^^^^^ definition snapshot-util 0.1 main/outer().middle().inner(). + nonlocal x + x = 1 +# ^ reference local 0 + return x +# ^ reference local 0 + + return inner +# ^^^^^ reference snapshot-util 0.1 main/outer().middle().inner(). + + return x, middle +# ^ reference local 0 +# ^^^^^^ reference snapshot-util 0.1 main/outer().middle(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/odd_pkg_name_1/main.py b/pyrefly/lib/report/scip/snapshots/output/odd_pkg_name_1/main.py new file mode 100644 index 0000000000..e732449a4e --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/odd_pkg_name_1/main.py @@ -0,0 +1,9 @@ +# < definition scip-python python package with space 0.1 main/__init__: + +def main(): +# ^^^^ definition package with space 0.1 main/main(). + pass + +main() +#^^^ reference package with space 0.1 main/main(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/project_site_packages/main.py b/pyrefly/lib/report/scip/snapshots/output/project_site_packages/main.py new file mode 100644 index 0000000000..0982800329 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/project_site_packages/main.py @@ -0,0 +1,11 @@ +# < definition scip-python python snapshot-util 0.1 main/__init__: + +# Its symbols should belong to review-dependency 2.0.0. +from review_dependency import Widget +# ^^^^^^^^^^^^^^^^^ reference review-dependency 2.0.0 review_dependency/__init__: +# ^^^^^^ reference review-dependency 2.0.0 review_dependency/Widget# + +widget = Widget() +#^^^^^ definition snapshot-util 0.1 main/widget. +# ^^^^^^ reference review-dependency 2.0.0 review_dependency/Widget# + diff --git a/pyrefly/lib/report/scip/snapshots/output/pyproject_1/main.py b/pyrefly/lib/report/scip/snapshots/output/pyproject_1/main.py new file mode 100644 index 0000000000..eb2e64294d --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/pyproject_1/main.py @@ -0,0 +1,9 @@ +# < definition scip-python python abc 16.05 main/__init__: + +def main(): +# ^^^^ definition abc 16.05 main/main(). + pass + +main() +#^^^ reference abc 16.05 main/main(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/relative_import/bar.py b/pyrefly/lib/report/scip/snapshots/output/relative_import/bar.py new file mode 100644 index 0000000000..2dc2f5f07a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/relative_import/bar.py @@ -0,0 +1,17 @@ +# < definition scip-python python snapshot-util 0.1 bar/__init__: + +from .foo import exported_function +# ^^^^ reference snapshot-util 0.1 foo/__init__: +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 foo/exported_function(). +from .foo import this_class +# ^^^^ reference snapshot-util 0.1 foo/__init__: +# ^^^^^^^^^^ reference snapshot-util 0.1 foo/this_class. + +if True: + exported_function() +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 foo/exported_function(). + + this_class.exported_function() +# ^^^^^^^^^^ reference snapshot-util 0.1 foo/this_class. +# ^^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 foo/MyClass#exported_function(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/relative_import/foo.py b/pyrefly/lib/report/scip/snapshots/output/relative_import/foo.py new file mode 100644 index 0000000000..3bbb6c7331 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/relative_import/foo.py @@ -0,0 +1,24 @@ +# < definition scip-python python snapshot-util 0.1 foo/__init__: + +def exported_function(): +# ^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 foo/exported_function(). + return "function" + +class MyClass: +# ^^^^^^^ definition snapshot-util 0.1 foo/MyClass# + """This is a class and it is cool""" + + def __init__(self): +# ^^^^^^^^ definition snapshot-util 0.1 foo/MyClass#__init__(). +# ^^^^ definition snapshot-util 0.1 foo/MyClass#__init__().(self) + pass + + def exported_function(self): +# ^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 foo/MyClass#exported_function(). +# ^^^^ definition snapshot-util 0.1 foo/MyClass#exported_function().(self) + return "exported" + +this_class = MyClass() +#^^^^^^^^^ definition snapshot-util 0.1 foo/this_class. +# ^^^^^^^ reference snapshot-util 0.1 foo/MyClass# + diff --git a/pyrefly/lib/report/scip/snapshots/output/request_goofiness/goofy.py b/pyrefly/lib/report/scip/snapshots/output/request_goofiness/goofy.py new file mode 100644 index 0000000000..89dac0fde5 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/request_goofiness/goofy.py @@ -0,0 +1,10 @@ +# < definition scip-python python snapshot-util 0.1 goofy/__init__: + +import requests +# ^^^^^^^^ reference requests 2.0.0 requests/__init__: + +print(requests.get("https://sourcegraph.com")) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^ reference requests 2.0.0 requests/__init__: +# ^^^ reference requests 2.0.0 `requests.api`/get(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/single_class/src/single_class.py b/pyrefly/lib/report/scip/snapshots/output/single_class/src/single_class.py new file mode 100644 index 0000000000..b9b89de9c0 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/single_class/src/single_class.py @@ -0,0 +1,40 @@ +# < definition scip-python python snapshot-util 0.1 single_class/__init__: + +class ExampleClass: +# ^^^^^^^^^^^^ definition snapshot-util 0.1 single_class/ExampleClass# + a: int +# ^ definition snapshot-util 0.1 single_class/ExampleClass#a. +# ^^^ reference python-stdlib 3.13 builtins/int# + b: int +# ^ definition snapshot-util 0.1 single_class/ExampleClass#b. +# ^^^ reference python-stdlib 3.13 builtins/int# + c: str +# ^ definition snapshot-util 0.1 single_class/ExampleClass#c. +# ^^^ reference python-stdlib 3.13 builtins/str# + + static_var = "Hello World" +# ^^^^^^^^^^ definition snapshot-util 0.1 single_class/ExampleClass#static_var. + + def __init__(self, a: int, b: int): +# ^^^^^^^^ definition snapshot-util 0.1 single_class/ExampleClass#__init__(). +# ^^^^ definition snapshot-util 0.1 single_class/ExampleClass#__init__().(self) +# ^ definition snapshot-util 0.1 single_class/ExampleClass#__init__().(a) +# ^^^ reference python-stdlib 3.13 builtins/int# +# ^ definition snapshot-util 0.1 single_class/ExampleClass#__init__().(b) +# ^^^ reference python-stdlib 3.13 builtins/int# + local_c = ", world!" +# ^^^^^^^ definition local 0 + + self.a = a +# ^^^^ reference snapshot-util 0.1 single_class/ExampleClass#__init__().(self) +# ^ reference snapshot-util 0.1 single_class/ExampleClass#a. +# ^ reference snapshot-util 0.1 single_class/ExampleClass#__init__().(a) + self.b = b +# ^^^^ reference snapshot-util 0.1 single_class/ExampleClass#__init__().(self) +# ^ reference snapshot-util 0.1 single_class/ExampleClass#b. +# ^ reference snapshot-util 0.1 single_class/ExampleClass#__init__().(b) + self.c = "hello" + local_c +# ^^^^ reference snapshot-util 0.1 single_class/ExampleClass#__init__().(self) +# ^ reference snapshot-util 0.1 single_class/ExampleClass#c. +# ^^^^^^^ reference local 0 + diff --git a/pyrefly/lib/report/scip/snapshots/output/single_function/src/single_function.py b/pyrefly/lib/report/scip/snapshots/output/single_function/src/single_function.py new file mode 100644 index 0000000000..0c9746a0aa --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/single_function/src/single_function.py @@ -0,0 +1,35 @@ +# < definition scip-python python snapshot-util 0.1 single_function/__init__: + +def my_cool_function(a: str) -> str: +# ^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 single_function/my_cool_function(). +# ^ definition snapshot-util 0.1 single_function/my_cool_function().(a) +# ^^^ reference python-stdlib 3.13 builtins/str# +# ^^^ reference python-stdlib 3.13 builtins/str# + x = ", world" +# ^ definition local 0 + return a + x +# ^ reference snapshot-util 0.1 single_function/my_cool_function().(a) +# ^ reference local 0 + +def my_cool_function_2(a: str): +# ^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 single_function/my_cool_function_2(). +# ^ definition snapshot-util 0.1 single_function/my_cool_function_2().(a) +# ^^^ reference python-stdlib 3.13 builtins/str# + x = ", world" +# ^ definition local 1 + return (lambda y: a + x + y)("oh no") +# ^ definition local 2 +# ^ reference snapshot-util 0.1 single_function/my_cool_function_2().(a) +# ^ reference local 1 +# ^ reference local 3 + +def next_level(): +# ^^^^^^^^^^ definition snapshot-util 0.1 single_function/next_level(). + return my_cool_function +# ^^^^^^^^^^^^^^^^ reference snapshot-util 0.1 single_function/my_cool_function(). + +my_cool_function("hello") +#^^^^^^^^^^^^^^^ reference snapshot-util 0.1 single_function/my_cool_function(). +next_level()(a = "wow") +#^^^^^^^^^ reference snapshot-util 0.1 single_function/next_level(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/type_parameters/main.py b/pyrefly/lib/report/scip/snapshots/output/type_parameters/main.py new file mode 100644 index 0000000000..68626af508 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/type_parameters/main.py @@ -0,0 +1,12 @@ +# < definition scip-python python snapshot-util 0.1 main/__init__: + +# The declaration of `T` and both annotations should share a defined symbol. +def identity[T](x: T) -> T: +# ^^^^^^^^ definition snapshot-util 0.1 main/identity(). +# ^ definition snapshot-util 0.1 main/identity().[T] +# ^ definition snapshot-util 0.1 main/identity().(x) +# ^ reference snapshot-util 0.1 main/identity().[T] +# ^ reference snapshot-util 0.1 main/identity().[T] + return x +# ^ reference snapshot-util 0.1 main/identity().(x) + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/builtin_import_refs.py b/pyrefly/lib/report/scip/snapshots/output/unique/builtin_import_refs.py new file mode 100644 index 0000000000..10f9e36703 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/builtin_import_refs.py @@ -0,0 +1,10 @@ +# < definition scip-python python snapshot-util 0.1 builtin_import_refs/__init__: + +from typing import Any +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# ^^^ reference python-stdlib 3.13 typing/Any. + +print(Any) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^ reference python-stdlib 3.13 typing/Any. + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/field_docstring.py b/pyrefly/lib/report/scip/snapshots/output/unique/field_docstring.py new file mode 100644 index 0000000000..8bb7a347f0 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/field_docstring.py @@ -0,0 +1,50 @@ +# < definition scip-python python snapshot-util 0.1 field_docstring/__init__: +#documentation ```python +# > (module) field_docstring +# > ``` + +# format-options: showDocs + +class ClassWithField: +# ^^^^^^^^^^^^^^ definition snapshot-util 0.1 field_docstring/ClassWithField# +# documentation ```python +# > (class) ClassWithField: type[ClassWithFi... +# > ``` + + a: int +# ^ definition snapshot-util 0.1 field_docstring/ClassWithField#a. +# documentation ```python +# > (variable) a: int +# > ``` +# > --- +# > Hello world, this is a +# ^^^ reference python-stdlib 3.13 builtins/int# +# external documentation ```python +# > (class) int: +# > @overload +# > def __new__(x: ConvertibleToInt = 0, /) ... +# > def __new__( +# > x: bytearray | bytes | str, +# > /, +# > base: SupportsIndex +# > ) -> int: ... +# > ``` +# > --- +# > int([x]) -> integer +# > int(x, base=10) -> integer +# > +# > Convert a number or string to an integer... +# > are given. If x is a number, return x._... +# > numbers, this truncates towards zero. +# > +# > If x is not a number or if base is given... +# > bytes, or bytearray instance representin... +# > given base. The literal can be preceded... +# > by whitespace. The base defaults to 10.... +# > Base 0 means to interpret the base from ... +# > ```python +# > >>> int('0b100', base=0) +# > ``` +# > 4 + """Hello world, this is a""" + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/grandparent_impl.py b/pyrefly/lib/report/scip/snapshots/output/unique/grandparent_impl.py new file mode 100644 index 0000000000..ec0f111d19 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/grandparent_impl.py @@ -0,0 +1,27 @@ +# < definition scip-python python snapshot-util 0.1 grandparent_impl/__init__: + +class A: +# ^ definition snapshot-util 0.1 grandparent_impl/A# + def grandparent(self) -> bool: +# ^^^^^^^^^^^ definition snapshot-util 0.1 grandparent_impl/A#grandparent(). +# ^^^^ definition snapshot-util 0.1 grandparent_impl/A#grandparent().(self) +# ^^^^ reference python-stdlib 3.13 builtins/bool# + return True + +class B(A): +# ^ definition snapshot-util 0.1 grandparent_impl/B# +# relationship implementation scip-python python snapshot-util 0.1 grandparent_impl/A# +# ^ reference snapshot-util 0.1 grandparent_impl/A# + ... + +class C(B): +# ^ definition snapshot-util 0.1 grandparent_impl/C# +# relationship implementation scip-python python snapshot-util 0.1 grandparent_impl/B# +# ^ reference snapshot-util 0.1 grandparent_impl/B# + def grandparent(self) -> bool: +# ^^^^^^^^^^^ definition snapshot-util 0.1 grandparent_impl/C#grandparent(). +# relationship implementation scip-python python snapshot-util 0.1 grandparent_impl/A#grandparent(). +# ^^^^ definition snapshot-util 0.1 grandparent_impl/C#grandparent().(self) +# ^^^^ reference python-stdlib 3.13 builtins/bool# + return False + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/inferred_field_docstring.py b/pyrefly/lib/report/scip/snapshots/output/unique/inferred_field_docstring.py new file mode 100644 index 0000000000..6992aeefec --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/inferred_field_docstring.py @@ -0,0 +1,64 @@ +# < definition scip-python python snapshot-util 0.1 inferred_field_docstring/__init__: +#documentation ```python +# > (module) inferred_field_docstring +# > ``` + +# format-options: showDocs + +class ClassWithInferredField: +# ^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField# +# documentation ```python +# > (class) ClassWithInferredField: type[Cla... +# > ``` + def __init__(self, b: int): +# ^^^^^^^^ definition snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#__init__(). +# documentation ```python +# > (method) __init__: def __init__( +# > self: Self@ClassWithInferredField, +# > b: int +# > ) -> None: ... +# > ``` +# ^^^^ definition snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#__init__().(self) +# documentation ```python +# > (parameter) self: Self@ClassWithInferred... +# > ``` +# ^ definition snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#__init__().(b) +# documentation ```python +# > (parameter) b: int +# > ``` +# ^^^ reference python-stdlib 3.13 builtins/int# +# external documentation ```python +# > (class) int: +# > @overload +# > def __new__(x: ConvertibleToInt = 0, /) ... +# > def __new__( +# > x: bytearray | bytes | str, +# > /, +# > base: SupportsIndex +# > ) -> int: ... +# > ``` +# > --- +# > int([x]) -> integer +# > int(x, base=10) -> integer +# > +# > Convert a number or string to an integer... +# > are given. If x is a number, return x._... +# > numbers, this truncates towards zero. +# > +# > If x is not a number or if base is given... +# > bytes, or bytearray instance representin... +# > given base. The literal can be preceded... +# > by whitespace. The base defaults to 10.... +# > Base 0 means to interpret the base from ... +# > ```python +# > >>> int('0b100', base=0) +# > ``` +# > 4 + self.b = b +# ^^^^ reference snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#__init__().(self) +# ^ definition snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#b. +# documentation ```python +# > (attribute) b: int +# > ``` +# ^ reference snapshot-util 0.1 inferred_field_docstring/ClassWithInferredField#__init__().(b) + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/inherits_class.py b/pyrefly/lib/report/scip/snapshots/output/unique/inherits_class.py new file mode 100644 index 0000000000..e85198f5f1 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/inherits_class.py @@ -0,0 +1,44 @@ +# < definition scip-python python snapshot-util 0.1 inherits_class/__init__: + +class A: +# ^ definition snapshot-util 0.1 inherits_class/A# + def x(self) -> int: +# ^ definition snapshot-util 0.1 inherits_class/A#x(). +# ^^^^ definition snapshot-util 0.1 inherits_class/A#x().(self) +# ^^^ reference python-stdlib 3.13 builtins/int# + raise NotImplemented +# ^^^^^^^^^^^^^^ reference python-stdlib 3.13 builtins/NotImplemented# + + def matched_despite_different_type(self, x: int): +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type(). +# ^^^^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type().(self) +# ^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type().(x) +# ^^^ reference python-stdlib 3.13 builtins/int# + pass + +class B(A): +# ^ definition snapshot-util 0.1 inherits_class/B# +# relationship implementation scip-python python snapshot-util 0.1 inherits_class/A# +# ^ reference snapshot-util 0.1 inherits_class/A# + def x(self) -> int: +# ^ definition snapshot-util 0.1 inherits_class/B#x(). +# relationship implementation scip-python python snapshot-util 0.1 inherits_class/A#x(). +# ^^^^ definition snapshot-util 0.1 inherits_class/B#x().(self) +# ^^^ reference python-stdlib 3.13 builtins/int# + return 5 + + def matched_despite_different_type(self, x: int, y: int): +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type(). +# relationship implementation scip-python python snapshot-util 0.1 inherits_class/A#matched_despite_different_type(). +# ^^^^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(self) +# ^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(x) +# ^^^ reference python-stdlib 3.13 builtins/int# +# ^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(y) +# ^^^ reference python-stdlib 3.13 builtins/int# + pass + + def unrelated(self): +# ^^^^^^^^^ definition snapshot-util 0.1 inherits_class/B#unrelated(). +# ^^^^ definition snapshot-util 0.1 inherits_class/B#unrelated().(self) + pass + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/module_docstring.py b/pyrefly/lib/report/scip/snapshots/output/unique/module_docstring.py new file mode 100644 index 0000000000..16cda68f8a --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/module_docstring.py @@ -0,0 +1,20 @@ +# < definition scip-python python snapshot-util 0.1 module_docstring/__init__: +#documentation ```python +# > (module) module_docstring +# > ``` +# > --- +# > This is a docstring for this module + +# format-options: showDocs + +""" +This is a docstring for this module +""" + +def x(): +# ^ definition snapshot-util 0.1 module_docstring/x(). +# documentation ```python +# > (function) x: def x() -> Literal[1]: ... +# > ``` + return 1 + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/multiinherits_test.py b/pyrefly/lib/report/scip/snapshots/output/unique/multiinherits_test.py new file mode 100644 index 0000000000..7b870fb1ed --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/multiinherits_test.py @@ -0,0 +1,60 @@ +# < definition scip-python python snapshot-util 0.1 multiinherits_test/__init__: + +class Left: +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Left# + def one(self) -> int: +# ^^^ definition snapshot-util 0.1 multiinherits_test/Left#one(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Left#one().(self) +# ^^^ reference python-stdlib 3.13 builtins/int# + return 1 + + def shared(self) -> bool: +# ^^^^^^ definition snapshot-util 0.1 multiinherits_test/Left#shared(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Left#shared().(self) +# ^^^^ reference python-stdlib 3.13 builtins/bool# + return False + +class Right: +# ^^^^^ definition snapshot-util 0.1 multiinherits_test/Right# + def two(self): +# ^^^ definition snapshot-util 0.1 multiinherits_test/Right#two(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Right#two().(self) + return 2 + + def shared(self) -> bool: +# ^^^^^^ definition snapshot-util 0.1 multiinherits_test/Right#shared(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Right#shared().(self) +# ^^^^ reference python-stdlib 3.13 builtins/bool# + return False + +class Multi(Left, Right): +# ^^^^^ definition snapshot-util 0.1 multiinherits_test/Multi# +# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Left# +# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Right# +# ^^^^ reference snapshot-util 0.1 multiinherits_test/Left# +# ^^^^^ reference snapshot-util 0.1 multiinherits_test/Right# + def one(self) -> int: +# ^^^ definition snapshot-util 0.1 multiinherits_test/Multi#one(). +# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Left#one(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#one().(self) +# ^^^ reference python-stdlib 3.13 builtins/int# + return 1 + + def two(self): +# ^^^ definition snapshot-util 0.1 multiinherits_test/Multi#two(). +# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Right#two(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#two().(self) + return 2 + + def three(self): +# ^^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#three(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#three().(self) + return 3 + + def shared(self) -> bool: +# ^^^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#shared(). +# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Left#shared(). +# ^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#shared().(self) +# ^^^^ reference python-stdlib 3.13 builtins/bool# + return True + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/property_access.py b/pyrefly/lib/report/scip/snapshots/output/unique/property_access.py new file mode 100644 index 0000000000..04770b64d3 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/property_access.py @@ -0,0 +1,40 @@ +# < definition scip-python python snapshot-util 0.1 property_access/__init__: + +from typing import Sequence +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# ^^^^^^^^ reference python-stdlib 3.13 typing/Sequence# + +class PropertyClass: +# ^^^^^^^^^^^^^ definition snapshot-util 0.1 property_access/PropertyClass# + def __init__(self): +# ^^^^^^^^ definition snapshot-util 0.1 property_access/PropertyClass#__init__(). +# ^^^^ definition snapshot-util 0.1 property_access/PropertyClass#__init__().(self) + pass + + @property +# ^^^^^^^^ reference python-stdlib 3.13 builtins/property# + def prop_ref(self): +# ^^^^^^^^ definition snapshot-util 0.1 property_access/PropertyClass#prop_ref(). +# ^^^^ definition snapshot-util 0.1 property_access/PropertyClass#prop_ref().(self) + return 5 + + +xs = [PropertyClass()] +#^ definition snapshot-util 0.1 property_access/xs. +# ^^^^^^^^^^^^^ reference snapshot-util 0.1 property_access/PropertyClass# + +def usage(xs: Sequence[PropertyClass]): +# ^^^^^ definition snapshot-util 0.1 property_access/usage(). +# ^^ definition snapshot-util 0.1 property_access/usage().(xs) +# ^^^^^^^^ reference python-stdlib 3.13 typing/Sequence# +# ^^^^^^^^^^^^^ reference snapshot-util 0.1 property_access/PropertyClass# + def nested(): +# ^^^^^^ definition snapshot-util 0.1 property_access/usage().nested(). + for x in xs: +# ^ definition local 0 +# ^^ reference snapshot-util 0.1 property_access/usage().(xs) + print(x.prop_ref) +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^ reference local 0 +# ^^^^^^^^ reference snapshot-util 0.1 property_access/PropertyClass#prop_ref(). + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/rebinding.py b/pyrefly/lib/report/scip/snapshots/output/unique/rebinding.py new file mode 100644 index 0000000000..1c5b9295c7 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/rebinding.py @@ -0,0 +1,85 @@ +# < definition scip-python python snapshot-util 0.1 rebinding/__init__: + +# Only the first binding of a symbol is its definition, except that every overload defines its +# parameters. +from typing import overload +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# ^^^^^^^^ reference python-stdlib 3.13 typing/overload(). + +x = 1 +# definition snapshot-util 0.1 rebinding/x. +x = 2 +# reference snapshot-util 0.1 rebinding/x. + + +def f(): +# ^ definition snapshot-util 0.1 rebinding/f(). + y = 1 +# ^ definition local 0 + y = 2 +# ^ reference local 0 + return y +# ^ reference local 0 + + +class C: +# ^ definition snapshot-util 0.1 rebinding/C# + a = 1 +# ^ definition snapshot-util 0.1 rebinding/C#a. + a = 2 +# ^ reference snapshot-util 0.1 rebinding/C#a. + + def __init__(self): +# ^^^^^^^^ definition snapshot-util 0.1 rebinding/C#__init__(). +# ^^^^ definition snapshot-util 0.1 rebinding/C#__init__().(self) + self.b = 1 +# ^^^^ reference snapshot-util 0.1 rebinding/C#__init__().(self) +# ^ definition snapshot-util 0.1 rebinding/C#b. + self.b = 2 +# ^^^^ reference snapshot-util 0.1 rebinding/C#__init__().(self) +# ^ reference snapshot-util 0.1 rebinding/C#b. + + @overload +# ^^^^^^^^ reference python-stdlib 3.13 typing/overload(). + def m(self, v: int) -> int: ... +# ^ definition snapshot-util 0.1 rebinding/C#m(). +# ^^^^ definition snapshot-util 0.1 rebinding/C#m().(self) +# ^ definition snapshot-util 0.1 rebinding/C#m().(v) +# ^^^ reference python-stdlib 3.13 builtins/int# +# ^^^ reference python-stdlib 3.13 builtins/int# + @overload +# ^^^^^^^^ reference python-stdlib 3.13 typing/overload(). + def m(self, v: str) -> str: ... +# ^ reference snapshot-util 0.1 rebinding/C#m(). +# ^^^^ definition snapshot-util 0.1 rebinding/C#m().(self) +# ^ definition snapshot-util 0.1 rebinding/C#m().(v) +# ^^^ reference python-stdlib 3.13 builtins/str# +# ^^^ reference python-stdlib 3.13 builtins/str# + def m(self, v): +# ^ reference snapshot-util 0.1 rebinding/C#m(). +# ^^^^ definition snapshot-util 0.1 rebinding/C#m().(self) +# ^ definition snapshot-util 0.1 rebinding/C#m().(v) + return v +# ^ reference snapshot-util 0.1 rebinding/C#m().(v) + + +def g(): ... +# ^ definition snapshot-util 0.1 rebinding/g(). +def g(): ... +# ^ reference snapshot-util 0.1 rebinding/g(). + + +def h(p): +# ^ definition snapshot-util 0.1 rebinding/h(). +# ^ definition snapshot-util 0.1 rebinding/h().(p) + p = 1 +# ^ reference snapshot-util 0.1 rebinding/h().(p) + return p +# ^ reference snapshot-util 0.1 rebinding/h().(p) + + +for x in range(3): +# ^ reference snapshot-util 0.1 rebinding/x. +# ^^^^^ reference python-stdlib 3.13 builtins/range# + pass + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/stdlib_backport.py b/pyrefly/lib/report/scip/snapshots/output/unique/stdlib_backport.py new file mode 100644 index 0000000000..360400da29 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/stdlib_backport.py @@ -0,0 +1,12 @@ +# < definition scip-python python snapshot-util 0.1 stdlib_backport/__init__: + +# typing_extensions is in the bundled stdlib stubs, but an installed distribution takes +# precedence. +from typing_extensions import Doc +# ^^^^^^^^^^^^^^^^^ reference typing_extensions 4.16.0 typing_extensions/__init__: +# ^^^ reference typing_extensions 4.16.0 typing_extensions/Doc# + +print(Doc) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^ reference typing_extensions 4.16.0 typing_extensions/Doc# + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/vars_inside_scopes.py b/pyrefly/lib/report/scip/snapshots/output/unique/vars_inside_scopes.py new file mode 100644 index 0000000000..299a86fe88 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/vars_inside_scopes.py @@ -0,0 +1,30 @@ +# < definition scip-python python snapshot-util 0.1 vars_inside_scopes/__init__: + +from typing import List +# ^^^^^^ reference python-stdlib 3.13 typing/__init__: +# ^^^^ reference python-stdlib 3.13 typing/List. + +class X: +# ^ definition snapshot-util 0.1 vars_inside_scopes/X# + items: List[int] +# ^^^^^ definition snapshot-util 0.1 vars_inside_scopes/X#items. +# ^^^^ reference python-stdlib 3.13 typing/List. +# ^^^ reference python-stdlib 3.13 builtins/int# + + def my_func(self): +# ^^^^^^^ definition snapshot-util 0.1 vars_inside_scopes/X#my_func(). +# ^^^^ definition snapshot-util 0.1 vars_inside_scopes/X#my_func().(self) + for x in self.items: +# ^ definition local 0 +# ^^^^ reference snapshot-util 0.1 vars_inside_scopes/X#my_func().(self) +# ^^^^^ reference snapshot-util 0.1 vars_inside_scopes/X#items. + y = x + 1 +# ^ definition local 1 +# ^ reference local 0 + + if 5 in self.items: +# ^^^^ reference snapshot-util 0.1 vars_inside_scopes/X#my_func().(self) +# ^^^^^ reference snapshot-util 0.1 vars_inside_scopes/X#items. + z = "oh ya" +# ^ definition local 2 + diff --git a/pyrefly/lib/report/scip/snapshots/output/unique/walrus.py b/pyrefly/lib/report/scip/snapshots/output/unique/walrus.py new file mode 100644 index 0000000000..263e42a125 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unique/walrus.py @@ -0,0 +1,67 @@ +# < definition scip-python python snapshot-util 0.1 walrus/__init__: + +import re +# ^^ reference python-stdlib 3.13 re/__init__: + +some_list = [1, 2, 3, 4, 5, 100, 1000] +#^^^^^^^^ definition snapshot-util 0.1 walrus/some_list. +pattern = "^[A-Za-z0-9_]*$" +#^^^^^^ definition snapshot-util 0.1 walrus/pattern. +text = "Some_name123" +#^^^ definition snapshot-util 0.1 walrus/text. + +# if statement +if (n := len(some_list)) > 10: +# ^ definition snapshot-util 0.1 walrus/n. +# ^^^ reference python-stdlib 3.13 builtins/len(). +# ^^^^^^^^^ reference snapshot-util 0.1 walrus/some_list. + print(f"List is too long with {n} elements.") +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^ reference snapshot-util 0.1 walrus/n. + +if (match := re.search(pattern, text)) is not None: +# ^^^^^ definition snapshot-util 0.1 walrus/match. +# ^^ reference python-stdlib 3.13 re/__init__: +# ^^^^^^ reference python-stdlib 3.13 re/search(). +# ^^^^^^^ reference snapshot-util 0.1 walrus/pattern. +# ^^^^ reference snapshot-util 0.1 walrus/text. + print("Found match:", match.group(0)) +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^ reference snapshot-util 0.1 walrus/match. +# ^^^^^ reference python-stdlib 3.13 re/Match#group(). + + +# comprehensions +def show_some_comprehension(): +# ^^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 walrus/show_some_comprehension(). + [print(x, root) for x in some_list if (x % 2 == 0) and (root := x**0.5) > 5] +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^ reference local 0 +# ^^^^ reference local 1 +# ^ definition local 0 +# ^^^^^^^^^ reference snapshot-util 0.1 walrus/some_list. +# ^ reference local 0 +# ^^^^ definition local 1 +# ^ reference local 0 + + +# while loop +while (line := input("Enter text: ")) != "quit": +# ^^^^ definition snapshot-util 0.1 walrus/line. +# ^^^^^ reference python-stdlib 3.13 builtins/input(). + print(f"You entered: {line}") +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^ reference snapshot-util 0.1 walrus/line. + + +# if + comprehension +if any((any_n := num) < 0 for num in some_list): +# ^^^ reference python-stdlib 3.13 builtins/any(). +# ^^^^^ definition snapshot-util 0.1 walrus/any_n. +# ^^^ reference local 2 +# ^^^ definition local 2 +# ^^^^^^^^^ reference snapshot-util 0.1 walrus/some_list. + print(f"Negative number found: {any_n}") +# ^^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^ reference snapshot-util 0.1 walrus/any_n. + diff --git a/pyrefly/lib/report/scip/snapshots/output/unresolved_import/unresolved.py b/pyrefly/lib/report/scip/snapshots/output/unresolved_import/unresolved.py new file mode 100644 index 0000000000..834950ffed --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/output/unresolved_import/unresolved.py @@ -0,0 +1,13 @@ +# < definition scip-python python snapshot-util 0.1 unresolved/__init__: + +import this_is_not_real +# ^^^^^^^^^^^^^^^^ reference local 0 + +print(this_is_not_real.x) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^^^^^^ reference local 0 +print(this_is_not_real.x) +#^^^^ reference python-stdlib 3.13 builtins/print(). +# ^^^^^^^^^^^^^^^^ reference local 0 + + diff --git a/pyrefly/lib/report/scip/snapshots/packageInfo.json b/pyrefly/lib/report/scip/snapshots/packageInfo.json new file mode 100644 index 0000000000..a1ed3e2533 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/packageInfo.json @@ -0,0 +1,16 @@ +{ + "default": { + "name": "snapshot-util", + "version": "0.1" + }, + "special": { + "odd_pkg_name_1": { + "name": "package with space", + "version": "0.1" + }, + "empty_project_name_version": { + "name": "", + "version": "" + } + } +} diff --git a/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/METADATA b/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/METADATA new file mode 100644 index 0000000000..a2f80ca6a8 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: requests +Version: 2.0.0 diff --git a/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/RECORD b/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/RECORD new file mode 100644 index 0000000000..3d91bfa356 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/site-packages/requests-2.0.0.dist-info/RECORD @@ -0,0 +1,3 @@ +requests/__init__.py,, +requests-2.0.0.dist-info/METADATA,, +requests-2.0.0.dist-info/RECORD,, diff --git a/pyrefly/lib/report/scip/snapshots/site-packages/requests/__init__.py b/pyrefly/lib/report/scip/snapshots/site-packages/requests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/METADATA b/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/METADATA new file mode 100644 index 0000000000..134c671c8e --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: typing_extensions +Version: 4.16.0 diff --git a/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/RECORD b/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/RECORD new file mode 100644 index 0000000000..f181eafd19 --- /dev/null +++ b/pyrefly/lib/report/scip/snapshots/site-packages/typing_extensions-4.16.0.dist-info/RECORD @@ -0,0 +1,3 @@ +typing_extensions.py,, +typing_extensions-4.16.0.dist-info/METADATA,, +typing_extensions-4.16.0.dist-info/RECORD,, diff --git a/pyrefly/lib/report/scip/symbols.rs b/pyrefly/lib/report/scip/symbols.rs new file mode 100644 index 0000000000..5b2aa09b30 --- /dev/null +++ b/pyrefly/lib/report/scip/symbols.rs @@ -0,0 +1,538 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::collections::HashMap; + +use pyrefly_python::ast::Ast; +use pyrefly_python::module_name::ModuleName; +use ruff_python_ast::Comprehension; +use ruff_python_ast::ExceptHandler; +use ruff_python_ast::Expr; +use ruff_python_ast::ExprAttribute; +use ruff_python_ast::ExprContext; +use ruff_python_ast::Identifier; +use ruff_python_ast::ModModule; +use ruff_python_ast::Parameters; +use ruff_python_ast::Pattern; +use ruff_python_ast::Stmt; +use ruff_python_ast::TypeParams; +use ruff_python_ast::name::Name; +use ruff_python_ast::visitor::source_order::SourceOrderVisitor; +use ruff_python_ast::visitor::source_order::walk_except_handler; +use ruff_python_ast::visitor::source_order::walk_expr; +use ruff_python_ast::visitor::source_order::walk_pattern; +use ruff_python_ast::visitor::source_order::walk_stmt; +use ruff_python_ast::visitor::source_order::walk_type_param; +use ruff_text_size::TextRange; +use scip::types::Descriptor; +use scip::types::descriptor::Suffix; +use starlark_map::small_set::SmallSet; + +use crate::report::glean::convert::gather_nonlocal_variables; + +#[derive(Debug, Clone, PartialEq)] +pub enum ScipSymbol { + Global(Vec), + Local(usize), +} + +/// The SCIP symbol of every name a module defines, keyed by the defining identifier, in source +/// order. +pub fn definitions(module: ModuleName, ast: &ModModule) -> Vec<(TextRange, ScipSymbol)> { + let module_path = vec![descriptor(module.as_str(), Suffix::Namespace)]; + let mut builder = Builder { + scopes: vec![Scope::new(ScopeKind::Module, module_path.clone())], + module_path, + definitions: Vec::new(), + next_local: 0, + }; + builder.visit_body(&ast.body); + builder.definitions +} + +pub fn descriptor(name: &str, suffix: Suffix) -> Descriptor { + Descriptor { + name: name.to_owned(), + suffix: suffix.into(), + ..Default::default() + } +} + +enum ScopeKind { + Module, + Class { + /// Names bound directly in the class body, plus `self.x` attributes seen so far. + attributes: SmallSet, + }, + Function { + receiver: Option, + globals: SmallSet, + nonlocals: SmallSet, + }, + /// Every name a comprehension binds is a local, except a walrus target. + Comprehension, + /// Every name a lambda binds is a local. + Lambda, +} + +struct Scope { + kind: ScopeKind, + path: Vec, + names: HashMap, +} + +impl Scope { + fn new(kind: ScopeKind, path: Vec) -> Self { + Self { + kind, + path, + names: HashMap::new(), + } + } +} + +struct Builder { + module_path: Vec, + scopes: Vec, + definitions: Vec<(TextRange, ScipSymbol)>, + next_local: usize, +} + +fn child(path: &[Descriptor], name: &str, suffix: Suffix) -> Vec { + let mut path = path.to_vec(); + path.push(descriptor(name, suffix)); + path +} + +impl Builder { + fn scope(&self) -> &Scope { + self.scopes + .last() + .expect("the module scope is never popped") + } + + fn new_local(&mut self) -> ScipSymbol { + self.next_local += 1; + ScipSymbol::Local(self.next_local - 1) + } + + fn define(&mut self, id: &Identifier, symbol: ScipSymbol) { + self.definitions.push((id.range, symbol)); + } + + /// Define a name in the current scope, regardless of any earlier binding of it. + fn define_in_scope(&mut self, id: &Identifier, symbol: ScipSymbol) { + self.scopes + .last_mut() + .expect("the module scope is never popped") + .names + .insert(id.id.clone(), symbol.clone()); + self.define(id, symbol); + } + + /// Define a function or class in the current scope, returning its path. + fn declare(&mut self, id: &Identifier, suffix: Suffix) -> Vec { + let path = child(&self.scope().path, &id.id, suffix); + self.define_in_scope(id, ScipSymbol::Global(path.clone())); + path + } + + /// The symbol for binding `name` in scope `index`, creating it on first use. + fn binding(&mut self, index: usize, name: &Name) -> ScipSymbol { + if let Some(symbol) = self.scopes[index].names.get(name) { + return symbol.clone(); + } + let symbol = match &self.scopes[index].kind { + ScopeKind::Module | ScopeKind::Class { .. } => { + ScipSymbol::Global(child(&self.scopes[index].path, name, Suffix::Term)) + } + ScopeKind::Function { globals, .. } if globals.contains(name) => { + ScipSymbol::Global(child(&self.module_path, name, Suffix::Term)) + } + ScopeKind::Function { nonlocals, .. } if nonlocals.contains(name) => { + // The nearest enclosing function that binds the name, or else the nearest one. + let functions: Vec = (0..index) + .rev() + .filter(|i| matches!(self.scopes[*i].kind, ScopeKind::Function { .. })) + .collect(); + match functions + .iter() + .find(|i| self.scopes[**i].names.contains_key(name)) + .or(functions.first()) + { + Some(enclosing) => self.binding(*enclosing, name), + None => self.new_local(), + } + } + ScopeKind::Function { .. } | ScopeKind::Comprehension | ScopeKind::Lambda => { + self.new_local() + } + }; + self.scopes[index] + .names + .insert(name.clone(), symbol.clone()); + symbol + } + + /// Bind a name in the current scope. A walrus inside a comprehension binds in the enclosing + /// scope instead. + fn bind(&mut self, id: &Identifier, walrus: bool) { + let mut index = self.scopes.len() - 1; + if walrus { + while index > 0 && matches!(self.scopes[index].kind, ScopeKind::Comprehension) { + index -= 1; + } + } + let symbol = self.binding(index, &id.id); + self.define(id, symbol); + } + + /// `self.x = ...` in a method defines the attribute `x`, unless the class already did. + fn bind_attribute(&mut self, attribute: &ExprAttribute) { + let [.., class, method] = self.scopes.as_mut_slice() else { + return; + }; + let ( + ScopeKind::Class { attributes }, + ScopeKind::Function { + receiver: Some(receiver), + .. + }, + ) = (&mut class.kind, &method.kind) + else { + return; + }; + if let Some(attr) = Ast::expr_receiver_attr(attribute, receiver) + && attributes.insert(attr.id.clone()) + { + let symbol = ScipSymbol::Global(child(&class.path, &attr.id, Suffix::Term)); + self.define(attr, symbol); + } + } + + fn bind_parameters(&mut self, parameters: &Parameters) { + for parameter in parameters.iter() { + let id = parameter.name(); + let symbol = match &self.scope().kind { + ScopeKind::Comprehension | ScopeKind::Lambda => self.new_local(), + _ => ScipSymbol::Global(child(&self.scope().path, &id.id, Suffix::Parameter)), + }; + self.define_in_scope(id, symbol); + } + } + + /// Type parameters belong to the function, class or type alias that declares them. + fn bind_type_params(&mut self, type_params: Option<&TypeParams>, owner: &ScipSymbol) { + for type_param in type_params.iter().flat_map(|x| &x.type_params) { + let symbol = match owner { + ScipSymbol::Global(path) => { + ScipSymbol::Global(child(path, &type_param.name().id, Suffix::TypeParameter)) + } + ScipSymbol::Local(_) => self.new_local(), + }; + self.define(type_param.name(), symbol); + walk_type_param(self, type_param); + } + } + + fn visit_comprehensions(&mut self, generators: &[Comprehension], elements: &[&Expr]) { + // The first iterable is evaluated in the enclosing scope. + if let Some(first) = generators.first() { + self.visit_expr(&first.iter); + } + self.scopes.push(Scope::new( + ScopeKind::Comprehension, + self.scope().path.clone(), + )); + for (i, generator) in generators.iter().enumerate() { + if i > 0 { + self.visit_expr(&generator.iter); + } + self.visit_expr(&generator.target); + for condition in &generator.ifs { + self.visit_expr(condition); + } + } + for element in elements { + self.visit_expr(element); + } + self.scopes.pop(); + } +} + +impl<'a> SourceOrderVisitor<'a> for Builder { + fn visit_stmt(&mut self, stmt: &'a Stmt) { + match stmt { + Stmt::FunctionDef(function) => { + for decorator in &function.decorator_list { + self.visit_decorator(decorator); + } + self.visit_parameters(&function.parameters); + if let Some(returns) = &function.returns { + self.visit_expr(returns); + } + let path = self.declare(&function.name, Suffix::Method); + self.bind_type_params( + function.type_params.as_deref(), + &ScipSymbol::Global(path.clone()), + ); + let receiver = match &self.scope().kind { + ScopeKind::Class { .. } => function + .parameters + .iter_non_variadic_params() + .next() + .map(|p| p.parameter.name.id.clone()), + _ => None, + }; + let (globals, nonlocals) = gather_nonlocal_variables(&function.body); + self.scopes.push(Scope::new( + ScopeKind::Function { + receiver, + globals: (*globals).clone(), + nonlocals: (*nonlocals).clone(), + }, + path, + )); + self.bind_parameters(&function.parameters); + self.visit_body(&function.body); + self.scopes.pop(); + } + Stmt::ClassDef(class) => { + for decorator in &class.decorator_list { + self.visit_decorator(decorator); + } + if let Some(arguments) = &class.arguments { + self.visit_arguments(arguments); + } + let path = self.declare(&class.name, Suffix::Type); + self.bind_type_params( + class.type_params.as_deref(), + &ScipSymbol::Global(path.clone()), + ); + let mut attributes = SmallSet::new(); + for stmt in &class.body { + match stmt { + Stmt::FunctionDef(x) => { + attributes.insert(x.name.id.clone()); + } + Stmt::ClassDef(x) => { + attributes.insert(x.name.id.clone()); + } + Stmt::Assign(x) => x.targets.iter().for_each(|t| { + Ast::expr_lvalue(t, &mut |n| { + attributes.insert(n.id.clone()); + }) + }), + Stmt::AnnAssign(x) => Ast::expr_lvalue(&x.target, &mut |n| { + attributes.insert(n.id.clone()); + }), + _ => {} + } + } + self.scopes + .push(Scope::new(ScopeKind::Class { attributes }, path)); + self.visit_body(&class.body); + self.scopes.pop(); + } + Stmt::TypeAlias(alias) => { + self.visit_expr(&alias.name); + if let Expr::Name(name) = &*alias.name + && let Some(owner) = self.scope().names.get(&name.id).cloned() + { + self.bind_type_params(alias.type_params.as_deref(), &owner); + } + self.visit_expr(&alias.value); + } + Stmt::AugAssign(assign) => { + if !matches!(&*assign.target, Expr::Name(_)) { + self.visit_expr(&assign.target); + } + self.visit_expr(&assign.value); + } + _ => walk_stmt(self, stmt), + } + } + + fn visit_expr(&mut self, expr: &'a Expr) { + match expr { + Expr::Name(name) if name.ctx == ExprContext::Store => { + self.bind(&Ast::expr_name_identifier(name.clone()), false) + } + Expr::Named(named) => { + if let Expr::Name(name) = &*named.target { + self.bind(&Ast::expr_name_identifier(name.clone()), true); + } + self.visit_expr(&named.value); + } + Expr::Attribute(attribute) if attribute.ctx == ExprContext::Store => { + self.bind_attribute(attribute); + self.visit_expr(&attribute.value); + } + Expr::ListComp(x) => self.visit_comprehensions(&x.generators, &[&x.elt]), + Expr::SetComp(x) => self.visit_comprehensions(&x.generators, &[&x.elt]), + Expr::Generator(x) => self.visit_comprehensions(&x.generators, &[&x.elt]), + Expr::DictComp(x) => { + let elements: Vec<&Expr> = + x.key.as_deref().into_iter().chain([&*x.value]).collect(); + self.visit_comprehensions(&x.generators, &elements) + } + Expr::Lambda(lambda) => { + if let Some(parameters) = &lambda.parameters { + self.visit_parameters(parameters); + } + self.scopes + .push(Scope::new(ScopeKind::Lambda, self.scope().path.clone())); + if let Some(parameters) = &lambda.parameters { + self.bind_parameters(parameters); + } + self.visit_expr(&lambda.body); + self.scopes.pop(); + } + _ => walk_expr(self, expr), + } + } + + fn visit_except_handler(&mut self, handler: &'a ExceptHandler) { + let ExceptHandler::ExceptHandler(x) = handler; + if let Some(name) = &x.name { + self.bind(name, false); + } + walk_except_handler(self, handler); + } + + fn visit_pattern(&mut self, pattern: &'a Pattern) { + let name = match pattern { + Pattern::MatchAs(x) => x.name.as_ref(), + Pattern::MatchStar(x) => x.name.as_ref(), + Pattern::MatchMapping(x) => x.rest.as_ref(), + _ => None, + }; + if let Some(name) = name { + self.bind(name, false); + } + walk_pattern(self, pattern); + } +} + +#[cfg(test)] +mod tests { + use pyrefly_python::ast::Ast; + use ruff_python_ast::PySourceType; + use scip::symbol::format_symbol; + use scip::types::Symbol; + + use super::*; + + fn symbols(code: &str) -> Vec<(String, String)> { + let (ast, _, _) = Ast::parse(code, PySourceType::Python); + definitions(ModuleName::from_str("m"), &ast) + .iter() + .map(|(range, symbol)| { + let symbol = match symbol { + ScipSymbol::Global(descriptors) => format_symbol(Symbol { + scheme: "s".to_owned(), + descriptors: descriptors.clone(), + ..Default::default() + }), + ScipSymbol::Local(i) => format!("local {i}"), + }; + (code[*range].to_owned(), symbol) + }) + .collect() + } + + fn assert_symbols(code: &str, expected: &[(&str, &str)]) { + let expected: Vec<_> = expected + .iter() + .map(|(name, symbol)| (name.to_string(), format!("s . . . {symbol}"))) + .collect(); + assert_eq!(symbols(code), expected); + } + + #[test] + fn test_classes_and_functions() { + assert_symbols( + r#" +class C: + x: int + class D: ... + def f(self, a, *args, **kwargs): + def g(): ... +"#, + &[ + ("C", "m/C#"), + ("x", "m/C#x."), + ("D", "m/C#D#"), + ("f", "m/C#f()."), + ("self", "m/C#f().(self)"), + ("a", "m/C#f().(a)"), + ("args", "m/C#f().(args)"), + ("kwargs", "m/C#f().(kwargs)"), + ("g", "m/C#f().g()."), + ], + ); + } + + #[test] + fn test_self_attributes() { + assert_symbols( + r#" +class C: + y: int + def __init__(self, v): + self.x = v + self.x = v + self.y = v +"#, + &[ + ("C", "m/C#"), + ("y", "m/C#y."), + ("__init__", "m/C#__init__()."), + ("self", "m/C#__init__().(self)"), + ("v", "m/C#__init__().(v)"), + ("x", "m/C#x."), + ], + ); + } + + #[test] + fn test_locals() { + let actual = symbols( + r#" +x = 1 +for y in []: pass +def f(p): + p = 1 + z = [w for w in []] + z = 2 + global x + x = 3 +squares = [w * w for w in []] +"#, + ); + let expected = [ + ("x", "s . . . m/x."), + ("y", "s . . . m/y."), + ("f", "s . . . m/f()."), + ("p", "s . . . m/f().(p)"), + ("p", "s . . . m/f().(p)"), + ("z", "local 0"), + ("w", "local 1"), + ("z", "local 0"), + ("x", "s . . . m/x."), + ("squares", "s . . . m/squares."), + ("w", "local 2"), + ]; + assert_eq!( + actual, + expected + .iter() + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect::>() + ); + } +} diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index da73d3e1e8..7c3b988eec 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -136,7 +136,7 @@ struct OperatorDunder { range: TextRange, } -fn callee_kind_from_call(call: &ExprCall) -> CalleeKind { +pub(crate) fn callee_kind_from_call(call: &ExprCall) -> CalleeKind { match call.func.as_ref() { Expr::Name(name) => CalleeKind::Function(Ast::expr_name_identifier(name.clone())), Expr::Attribute(attr) => CalleeKind::Method(attr.value.range(), attr.attr.clone()), @@ -1274,7 +1274,7 @@ impl<'a> Transaction<'a> { } } let mut res = None; - mod_module.visit(&mut |x| f(x, position, &mut res)); + Ast::visit_covering_exprs(&mod_module, position, &mut |x| f(x, position, &mut res)); res } @@ -2887,6 +2887,24 @@ impl<'a> Transaction<'a> { identifier: &Identifier, callee_kind: &CalleeKind, preference: FindPreference, + ) -> Vec { + self.find_definition_for_keyword_argument_with_ast( + handle, + identifier, + callee_kind, + preference, + |module| self.get_ast_or_parse_module(handle, module), + ) + } + + /// Like `find_definition_for_keyword_argument`, but gets the callee module's AST from `get_ast`. + pub(crate) fn find_definition_for_keyword_argument_with_ast( + &self, + handle: &Handle, + identifier: &Identifier, + callee_kind: &CalleeKind, + preference: FindPreference, + get_ast: impl Fn(&ModuleInfo) -> Arc, ) -> Vec { // NOTE(grievejia): There might be a better way to compute this that doesn't require 2 containing node // traversal, once we gain access to the callee function def from callee_kind directly. @@ -2906,7 +2924,7 @@ impl<'a> Transaction<'a> { let mut results: Vec = Vec::with_capacity(location_count); for (module_info, ranges) in modules_to_ranges.into_iter() { - let ast = self.get_ast_or_parse_module(handle, &module_info); + let ast = get_ast(&module_info); for range in ranges.into_iter() { let (metadata, definition_range) = if let Some((definition_range, metadata)) = self diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index 696e29096b..dd31a102e2 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -717,6 +717,9 @@ pub(crate) struct TransactionData<'a> { cinderx_reporter: Option>, /// When set, called per solved module while its answers are still live (before eviction). solutions_hook: Option>, + /// When set, the answers of a module are evicted once the solutions hook has seen it, + /// whatever its require level. + evict_after_solutions_hook: bool, } impl<'a> TransactionData<'a> { @@ -869,6 +872,12 @@ impl<'a> Transaction<'a> { self.data.solutions_hook = hook; } + /// Evict each module's answers once the solutions hook has seen it, so a hook that needs + /// them at `Require::Everything` doesn't keep them for the rest of the run. + pub fn set_evict_after_solutions_hook(&mut self, evict: bool) { + self.data.evict_after_solutions_hook = evict; + } + /// Take the pysa reporter out of the transaction, consuming ownership. pub fn take_pysa_reporter(&mut self) -> Option> { self.data.pysa_reporter.take() @@ -960,6 +969,15 @@ impl<'a> Transaction<'a> { self.with_module_inner(handle, |x| x.get_ast()) } + /// Run `op` on the state's thread pool, which has an increased stack size. + pub fn install(&self, op: OP) -> R + where + OP: FnOnce() -> R + Send, + R: Send, + { + self.data.state.threads.install(op) + } + pub(crate) fn endpoint_module_cache(&self) -> &EndpointModuleCache { &self.data.state.endpoint_module_cache } @@ -1683,7 +1701,7 @@ impl<'a> Transaction<'a> { if let Some(hook) = &self.data.solutions_hook { hook(&module_data.handle, self); } - if !require.keep_answers() { + if !require.keep_answers() || self.data.evict_after_solutions_hook { // From now on we can use the solutions directly, so evict the answers. post.evict_answers(); } @@ -3677,6 +3695,7 @@ impl State { pysa_reporter: None, cinderx_reporter: None, solutions_hook: None, + evict_after_solutions_hook: false, }, } } @@ -3781,6 +3800,7 @@ impl State { pysa_reporter: _, cinderx_reporter: _, solutions_hook: _, + evict_after_solutions_hook: _, }, }, committing_transaction_guard, diff --git a/pyrefly/lib/test/lsp/hover.rs b/pyrefly/lib/test/lsp/hover.rs index d97005d411..69ebcc1ef9 100644 --- a/pyrefly/lib/test/lsp/hover.rs +++ b/pyrefly/lib/test/lsp/hover.rs @@ -20,6 +20,7 @@ use pyrefly_python::sys_info::PythonVersion; use ruff_text_size::TextSize; use crate::lsp::wasm::hover::HoverOptions; +use crate::lsp::wasm::hover::HoverResult; use crate::lsp::wasm::hover::get_hover; use crate::lsp::wasm::hover::get_hover_with_verbosity; use crate::state::require::Require; @@ -89,6 +90,7 @@ fn get_test_report_at_verbosity( HoverOptions { show_go_to_links: true, verbosity_level, + field_docstring_at_definition: false, }, ) { Some(result) => match result.hover { @@ -1295,6 +1297,43 @@ widget.box ); } +#[test] +fn hover_shows_field_docstring_at_definition_when_enabled() { + let code = r#" +class Widget: + name: str +# ^ + """Name of the widget.""" +"#; + let mut env = TestEnv::new(); + env.add("main", code); + let (state, handle_for_name) = env.to_state(); + let handle = handle_for_name("main"); + let position = extract_cursors_for_test(code)[0]; + let hover = |field_docstring_at_definition| { + let options = HoverOptions { + show_go_to_links: false, + verbosity_level: 0, + field_docstring_at_definition, + }; + match get_hover_with_verbosity(&state.transaction(), &handle, position, options) { + Some(HoverResult { + hover: + Hover { + contents: Contents::MarkupContent(markup), + .. + }, + .. + }) => markup.value, + _ => "None".to_owned(), + } + }; + let enabled = hover(true); + assert!(enabled.contains("Name of the widget."), "got: {enabled}"); + let disabled = hover(false); + assert!(!disabled.contains("Name of the widget."), "got: {disabled}"); +} + #[test] fn hover_parameter_doc_with_complex_types() { let code = r#" diff --git a/ruff.toml b/ruff.toml index 1fc686fb8a..7d891a40c0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,6 +5,7 @@ extend-exclude = [ "conformance/**", "crates/pyrefly_bundled/**", "crates/tsp_types/**", + "pyrefly/lib/report/scip/snapshots/**", "pyrefly/lib/test/**", "test/**", "website/**",