Sam Goldman
samgoldman@meta.com
90d · built 2026-05-28
90-day totals
- Commits
- 95
- Grow
- 5.7
- Maintenance
- 6.4
- Fixes
- 1.0
- Total ETV
- 13.1
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 51 %
- By Growth share
- Top 76 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Bug flow over time
Monthly bug flow attributed to this developer. The left bar (red) is bug impact this dev authored that was addressed in the given month — combining bugs others fixed for them and bugs they fixed themselves. The right bar is fixes they personally shipped that month, split between self-fixes (overlap with the red bar) and fixes done for someone else. X-axis is fix-time, not introduction-time — the Navigara API attributes bugs backward to the author at the moment the fix lands.
- Self-fix share
- 17%
- Bugs you introduced
- 8.7
- Bugs you fixed
- 6.4
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 20 by ETV in the 90-day window.
- 1.2ETVAdd laziness test suite for type checking dependencies (#3219) Summary: Pull Request resolved: https://github.com/facebook/pyrefly/pull/3219 Adds a snapshot-based test suite documenting how much of a file's dependencies get type-checked when the file itself is checked. The laziness goal is that dependencies should stop at the earliest step (Load/Ast/Exports/Answers/Solutions) sufficient to resolve what callers actually need — e.g. a transitively-imported module used only as an annotation should not have its body inferred. Each test sets up a small module graph, checks one entry-point module, and records in a markdown snapshot the highest step each module reached plus the tree of cross-module demands observed. The snapshots capture current behavior — some document good laziness properties, others document opportunities where dependencies are computed further than callers need. Snapshots regenerate with `UPDATE_SNAPSHOTS=1`. Supporting infrastructure: - `DemandCollector` (`pyrefly_util::demand_tree`) records cross-module `LookupExport`/`LookupAnswer` events into a tree. Scoped to a single `Transaction` via `set_demand_collector` so parallel checks don't interfere. `enter()` returns an RAII span guard so the per-thread nesting stack stays balanced even when an Answer lookup panics. - `pyrefly check --report-demand-tree <path>` emits a JSON document (`{ module_steps, demand_tree }`). - The test harness renders the structured tree into a flat indented form for snapshot comparison, and aggregates pervasive `builtins`/`typing` demands into a single count so test output stays readable. Reviewed By: kinto0 Differential Revision: D102239243 fbshipit-source-id: 527e24d494c2dad8b9f43393833a5ef04ae59b34github.com-facebook-pyrefly · f5250365 · 2026-05-04
- 0.9ETVversioned typeErrorDisplayStatus Summary: The status bar UI in commit 9 needs richer information than the legacy `pyrefly/textDocument/typeErrorDisplayStatus` request returns: a preset label, a markdown tooltip, and a docs URL. Bumping the response shape unconditionally would break older VSIX clients that parse the bare string. So we version the wire shape and let the client opt in. - `TypeErrorDisplayStatusVersion` (V1, V2) is parsed from `initializationOptions.pyrefly.typeErrorDisplayStatusVersion`. The server clamps unknown future values to V1 — the safest fallback, since every historical client can already decode V1's bare string. Missing or null also resolves to V1. - `TypeErrorDisplayStatusV2` is the rich shape: `{ version: "v2", label, tooltip, docsUrl }`. `label` drives the status-bar parenthetical (`Pyrefly (Basic)` etc.); `null` means show plain `Pyrefly`. - `TypeErrorDisplayStatusResponse` is the sum of the two; a hand Serialize impl emits whichever wire shape was chosen so V1 stays byte-identical to the historical response. - `derive_v2_response` is a free function taking the resolved config's `synthesized_preset_reason`, `source`, `disable_type_errors_in_ide` flag, AND the workspace `disable_type_errors` kill switch. It owns the V2 derivation rules and is unit-tested against synthetic inputs. - Derivation rules: - Workspace kill switch on (highest priority) → `Pyrefly (Errors Off)` + tooltip pointing at `python.pyrefly.disableTypeErrors`. - `IdeOverride` reason → `null` label, tooltip notes the workspace `typeCheckingMode` setting. - `Migrated(Mypy(...))` → `Legacy` label, tooltip references the source (`mypy.ini` or `[tool.mypy]` in `pyproject.toml`) and `pyrefly init`. - `Migrated(Pyright(...))` → `Default` label, tooltip references the source (`pyrightconfig.json` or `[tool.pyright]` in `pyproject.toml`) and `pyrefly init`. - `NoNearbyConfig` → `Basic` label, generic onboarding tooltip. - File source + in-config `disable-type-errors-in-ide` → `Errors Off` label, config-flavored tooltip pointing at the project's `pyrefly.toml`. - File source + errors enabled → `null` label, no tooltip (a fully configured project shouldn't be nudged). Both kill-switch branches use the same `Errors Off` parenthetical so users learn the state from the status bar itself; the tooltip distinguishes workspace-level from config-level disable so they know which knob to flip. Reviewed By: stroxler Differential Revision: D103653827 fbshipit-source-id: b4e90ebb89ce36757389996f3735bfe29f7a14cagithub.com-facebook-pyrefly · eaa973f0 · 2026-05-05
- 0.7ETVCLI stderr upsell after pyrefly check Summary: With commit 3, `pyrefly check` on an unconfigured project now produces results derived from a synthesized `ConfigFile` whose `synthesized_preset_reason` records *why* that config was synthesized. This commit surfaces that signal to the user: after the existing summary block in `CheckArgs::run_inner`, we walk every checked handle's config and emit a short upsell to **stderr** explaining what's going on and pointing at `pyrefly init`. The upsell is unconditional — it ignores `--output-format` and the ordinary error count. Routing it to stderr means machine-readable stdout formats like `json` and `omit-errors` stay untouched, so CI integrations don't break. Per-reason copy: - `Migrated(Mypy(DedicatedFile))` — "using settings imported from your `mypy.ini` (preset: legacy)" + "Run `pyrefly init` to continue setting up Pyrefly." + docs URL. - `Migrated(Mypy(PyprojectToml))` — same wording but "from `[tool.mypy]` in your `pyproject.toml`". - `Migrated(Pyright(...))` — analogous for `pyrightconfig.json` / `[tool.pyright]` in `pyproject.toml` (preset: default). - `NoNearbyConfig` — "using preset `basic`" + "Run `pyrefly init` to continue setting up Pyrefly." + docs URL. - `IdeOverride` — silenced. The user has explicitly chosen a behavior via the IDE setting; nagging them would be noise. Also keeps the CLI copy honest in case the LSP-only reason ever leaks here. Implementation factors the formatting into a pure `write_unconfigured_upsell(reasons, out)` so it can be unit-tested against a `Vec<u8>` without a full check run. The call site collects unique reasons across handles into a `SmallSet` and feeds them to the formatter; this de-dupes the (common) case where every handle in a single-root project shares the same reason. A `Hash` derive was added to `SynthesizedPresetReason` (and the `MigratedFromKind` / `MigratedConfigSource` it carries) so it can live in a `SmallSet`. Reviewed By: stroxler Differential Revision: D103653828 fbshipit-source-id: 337fce408c678b60a9479c9d09f2de2d374f8e06github.com-facebook-pyrefly · 3f205643 · 2026-05-05
- 0.5ETVworkspace setting typeCheckingMode Summary: Adds plumbing for the new IDE settings the resolver and the LSP filter need. Splits the legacy `displayTypeErrors` setting onto two new axes so the type-checking-mode choice and the workspace-wide kill switch can be controlled independently. - `python.pyrefly.typeCheckingMode` (enum: auto / off / basic / legacy / default / strict, default `auto`). Controls what preset Pyrefly synthesizes for files not covered by a `pyrefly.toml` or `[tool.pyrefly]` section — those configurations always take precedence over this setting. Read by the resolver in commit 6. - `python.pyrefly.disableTypeErrors` (boolean, default unset). Pure workspace-scoped kill switch. `true` suppresses every diagnostic for files in this workspace; absent (or `false`) defers to the project's `pyrefly.toml` and any in-config `disable-type-errors-in-ide` flag. Read by the LSP `type_error_display_status` filter in commit 8. Both fields are deserialized in `PyreflyClientConfig` and stored on `Workspace`. `apply_client_configuration` calls each `update_*` helper only when the corresponding `resolve_*` returns `Some` — that's a load-bearing detail: a `did_change_configuration` payload that omits both legacy and new fields must NOT clobber a prior workspace override or trigger a config-cache recheck. Backwards-compat mapping for the legacy setting splits across the two axes: - `force-on` → `typeCheckingMode = "default"`. No-op on the kill switch. - `force-off` → `disableTypeErrors = true` (workspace kill switch). No-op on the typeCheckingMode axis (the kill switch covers all cases). - `default` / `error-missing-imports` → reset both axes to their unset values (`Auto` / `false`). These legacy values carry no semantic meaning, but the resolver returns `Some(reset)` rather than `None` so a user moving from `force-on` (or `force-off`) back to `default` actually clears the prior workspace override — returning `None` would leave the stale `Default` / `true` sticking around. Note: legacy `displayTypeErrors = "force-on"` historically *also* pierced an in-config `disable-type-errors-in-ide = true` to force errors visible. That override is dropped — `disableTypeErrors` is a clean two-state boolean, so there's no way to express "force show even when the project disables." Users who relied on the override should remove the in-config disable from their `pyrefly.toml`. When both legacy and new settings are present on the same axis, the new setting wins. Reviewed By: grievejia Differential Revision: D103653833 fbshipit-source-id: a1501ef15040fb942a1d83d116e5e7fb1c564b47github.com-facebook-pyrefly · 11922219 · 2026-05-05
- 0.5ETVAdd BindingsMetadata for fast cross-module class field lookup Summary: Introduce a BindingsMetadata struct populated during the bindings phase that stores per-class metadata (starting with fields) in a Vec indexed by ClassDefIndex. This enables efficient metadata lookups without going through the solve/calculation code paths. - Move class fields from ClassBinding/FunctionalClassDef into BindingsMetadata, which is wrapped in an Arc and stored in both Bindings and Solutions for availability after answer eviction. - Add get_bindings_metadata to the LookupAnswer trait, implemented on TransactionHandle using the Guard pattern to avoid Arc refcount contention. - Add a metadata cache (UnsafeCell<FxHashMap>) on AnswersSolver with get_class_fields() for zero-clone same-module lookups and at-most-one Arc clone per cross-module. - Fields remain on ClassInner for now; a follow-up will migrate callers to the new API. Performance (median of 3 opt-clang-thinlto runs, baseline=parent of stack): PyTorch: | metric | baseline | this diff | cumulative | |--------|----------|-----------|------------| | cpu | 40.65s | 41.20s | +1.4% | | wall | 2.24s | 2.23s | -0.4% | | maxrss | 0.93GB | 0.93GB | +0.0% | Reviewed By: yangdanny97 Differential Revision: D96515733 fbshipit-source-id: 5a5a58b8446e4612053e102d9ae38beee71e4e06github.com-facebook-pyrefly · d6eb7d6f · 2026-03-16
- 0.5ETVChange find_definition to return Result<Vec1, EmptyResponseReason> Summary: Context: This stack improves diagnostics for empty LSP navigation results (go-to-definition, hover, etc.). Previously, when the server returned null, we could not distinguish expected cases (user clicked on whitespace) from unexpected failures (module not found, internal bugs). These changes add structured EmptyResponseReason telemetry so we can identify and fix the root causes of failed navigations. This Diff: Change find_definition from returning Vec<FindDefinitionItemWithDocstring> to Result<Vec1<...>, EmptyResponseReason>. Vec1 guarantees the Ok result is non-empty; empty results are now Err with a specific reason: AstNotFound, NotAnIdentifier, DefinitionNotFound, BindingsNotFound, ModuleInfoNotFound, AnswersNotFound, TypeTraceNotFound, or ModuleNotFound. Helper methods also updated to return Result with specific error reasons. All callers updated with .map(Vec1::into_vec).unwrap_or_default(). Reviewed By: grievejia Differential Revision: D98222001 fbshipit-source-id: 3e4284bd0b5208719d7c15db519fb00911aaac99github.com-facebook-pyrefly · a826045c · 2026-03-26
- 0.5ETVAdd configuration presets (off, basic, legacy, default, strict) Summary: Add a `preset` config option that provides named collections of error severities and behavior settings as the base configuration. User-specified settings always override the preset, regardless of order in the config file. Presets (listed from least to most strict): - **off**: Silences every error kind. Other settings are left at their defaults. Useful when Pyrefly is running only for IDE features like hover and go-to-definition, without diagnostics. - **basic**: An opt-in, low-noise preset for unconfigured projects and LSP users. Only high-confidence diagnostics — crashes and clearly broken code — fire; every other error kind is silenced. Enables as errors: `division-by-zero`, `invalid-syntax`, `missing-import`, `parse-error`, `unexpected-keyword`, `unknown-name`, `invalid-annotation`, `not-async`, `unused-coroutine`. Sets `check-unannotated-defs = false`, `infer-return-types = "never"`, `infer-with-first-use = false`, `permissive-ignores = true`. - **legacy**: A looser, less-strict preset intended for codebases migrating from mypy. Disables `bad-override-mutable-attribute` and `bad-override-param-name`. Sets `check-unannotated-defs = false`, `infer-return-types = "never"`. Named `legacy` rather than `mypy` to avoid implying emulation parity — Pyrefly's type-checking behavior still differs from mypy. The disabled checks are ones mypy does not have, so migrating users aren't hit with new errors for classes of issues mypy never flagged. - **default**: Current behavior (no-op). Equivalent to having no preset. - **strict**: Enables additional error codes (`implicit-any`, `unannotated-parameter`, `unannotated-return`, etc.) and `strict-callable-subtyping`. The preset is applied early in `configure()`: preset errors become the base with user errors merged on top — with parent-kind and deprecated-alias cascade handling so user overrides like `bad-override = "error"` take effect over the preset's child entries — and preset scalar settings fill in any fields the user didn't explicitly set. Presets only populate `ConfigFile::root`; sub-configs inherit through the existing root-fallback pattern in the per-field accessors, and the same cascade rules apply when sub-config overrides merge into root errors. The mypy migration (`pyrefly init`) now sets `preset = "legacy"` instead of explicitly listing equivalent settings. A doc sync test ensures every preset is documented in configuration.mdx and that documented error codes are consistent with `Preset::apply()`. Reviewed By: stroxler Differential Revision: D101067034 fbshipit-source-id: 1ae69f69cde8a99036f53ea7d3086819c9bdf559github.com-facebook-pyrefly · 6db53459 · 2026-04-29
- 0.4ETVEliminate busy-wait in demand loop Summary: This diff re-introduces D95144169, updated to fix a deadlock that led to the revert diff D95256215. As before, the primary goal of this change is to simplify the locking mechanism and eliminate a potential busy-wait loop. After the fix, the change is now even simpler than before. The description below explains how the deadlock happened in the previous iteration of this diff and how this updated version fixes it. P2220092266 contains just the changes on top of D95144169 with the fix, which might be easier to review. The main improvements: * Re-checking the condition inside a mutex to avoid race condition between checking the condition and acquiring the lock * Simpler invariants for try_start_clean and try_start_compute, which now both return None when there is no work to do. Before, callers needed to re-check the condition in the None case. * Removing unnecessary checks, relying on the simpler invariants above. Specifically, (1) hoisting the clean check out of the loop, so we only check once, and (2) removing the check for next step after acquiring the compute guard -- the guard now contains the step it is going to compute. ## Deadlock in keyless ExclusiveLock The keyless `ExclusiveLock` blocks unconditionally when held (`Once::wait`). In the demand loop, there's a race between checking `next_step` and acquiring the lock: 1. Thread A checks `next_step(M)` → sees `Exports` (needs computing) 2. Another thread computes Exports on M, then starts computing Answers (re-acquires lock) 3. Thread A calls `ExclusiveLock::lock(M)` → blocks on `Once::wait` until Answers finishes Thread A only needed Exports (which is now done), but it's stuck waiting for Answers. If thread A holds a lock on a different module that the Answers computation needs, we get a deadlock cycle. The old keyed lock avoided this: `lock(Exports)` on a lock held with key `Answers` returned `None` immediately (key mismatch), so thread A would re-check `next_step`, see Exports was done, and move on. ## The fix Replace `ExclusiveLock` with a `Mutex<bool>` (computing flag) + `Condvar`. The step check and lock acquisition happen atomically under the same mutex: ```rust let mut computing = self.computing.lock(); loop { if step_already_done { return None; } if !*computing { *computing = true; return Some(guard); } computing = self.computing_condvar.wait(computing); } ``` A thread waiting for Exports will wake up, re-check `next_step` under the mutex, see Exports is done, and return `None` — never blocking on the wrong step. The deadlock cycle cannot form because threads only block while their needed step is genuinely not yet computed. Reviewed By: grievejia Differential Revision: D95402906 fbshipit-source-id: 3861d48b4420cc335f394de6d8d595187e3f8a79github.com-facebook-pyrefly · 4b04ce6f · 2026-03-05
- 0.4ETVin-memory migration helper + SynthesizedPresetReason Summary: This is the foundation for the onboarding-experience work in this stack. The stack re-shapes Pyrefly's behavior in the unconfigured state by synthesizing a `ConfigFile` whose preset is chosen from auto-detected mypy/pyright config (full in-memory migration) or otherwise the new `Basic` preset. Both the LSP status bar and the CLI upsell need to know *why* a synthesized config has the preset it does, so the resolver introduced in the next commit can label the config with that reason. This commit is pure plumbing — no behavior changes yet: - Adds `SynthesizedPresetReason` to `pyrefly_config::config` with variants `NoNearbyConfig`, `Migrated(MigratedFromKind)`, and `IdeOverride`. Stored on `ConfigFile` as a runtime-only field (`#[serde(skip)]`, ignored for `PartialEq`) since it never survives a round-trip through TOML. - Adds `MigratedFromKind` to `pyrefly_config::migration::run`. The enum carries a `MigratedConfigSource` (`DedicatedFile` vs `PyprojectToml`) so consumers can render "your `mypy.ini`" vs "`[tool.mypy]` in your `pyproject.toml`" in the upsell and status-bar tooltip. - Adds `find_and_migrate_in_memory(start)` to the same module. The function searches upward for mypy.ini / pyrightconfig.json / pyproject.toml, runs the existing in-memory migration, and returns the resulting `ConfigFile` plus the source kind. No files are written. Returns `Ok(None)` when no migrate-able config exists; propagates `Err` on parse failure so callers can decide whether to fall back to a synthesized preset. - Refactors the previously private `Args::find_config` into a free function `find_upward_config` returning `Option<PathBuf>`, since both the new helper and the existing `Args::load_config` need it. The old `Err`-on-not-found behavior is preserved at `Args::load_config`'s call site. Reviewed By: stroxler Differential Revision: D103653834 fbshipit-source-id: e99a20ddb44516e21b659a48713e3105708770fegithub.com-facebook-pyrefly · 481e2c36 · 2026-05-05
- 0.4ETVCombine computed epoch and dirty flags into AtomicComputedDirty Summary: Pack the `computed` epoch (u32) and `dirty` flags (u8) into a single `AtomicU64` so that `try_mark_deps_dirty` can atomically check `computed != now` and set the DEPS flag in a single CAS operation. With separate atomics, these would be two operations with no way to make them atomic together. Changes: - Add `AtomicComputedDirty` in dirty.rs combining epoch and dirty flags - Add `as_u32`/`from_u32` accessors to `Epoch` - Update `ModuleStateMut` to use `AtomicComputedDirty` instead of separate `computed` and `dirty` fields - Add `set_dirty_load/find/deps` accessor methods to `ModuleStateMut` - Remove the now-unused `AtomicDirty` type and `AtomicEpoch::store_relaxed` Reviewed By: kinto0 Differential Revision: D95104945 fbshipit-source-id: df4b3996a501143ed349b053d4aecf28b22ae6d2github.com-facebook-pyrefly · 0044be12 · 2026-03-04
- 0.3ETVAdd bad-override-mutable-attribute error code Summary: Mutable (read-write) attributes require invariant types when overridden, unlike methods or read-only attributes which allow covariant overrides. Pyrefly correctly enforces this, but mypy does not by default (it requires the `mutable-override` option). To let users match mypy's behavior, emit these errors under a new `bad-override-mutable-attribute` code that can be independently disabled. This is a sub-kind of `bad-override`: suppressing `bad-override` also suppresses `bad-override-mutable-attribute`. The sub-kind mechanism is implemented via `ErrorKind::suppression_names()` and `ErrorKind::parent_kind()`, which are used in: - Inline suppression matching (`# pyrefly: ignore[bad-override]`) - Config-based severity (`ErrorDisplayConfig::severity()` falls back to parent kind) - Unused-ignore tracking (parent kind counts as "used") - Hover suppression display (shows sub-kind errors suppressed by parent kind) - F-string suppression Cases classified as `bad-override-mutable-attribute`: - `ReadWrite` attribute overriding a `ReadWrite` attribute with narrowed type (`Invariant` error) - `ReadWrite` attribute overriding a readwrite `Property` with narrowed type (`Contravariant` with `got_is_property=false, want_is_property=true`) - Decorated method where the decorator returns a scalar (becoming a mutable attribute) Reviewed By: grievejia Differential Revision: D100861912 fbshipit-source-id: bc51912d6e05d38ff2591336ca4d3bd569b07899github.com-facebook-pyrefly · 63ab0188 · 2026-04-16
- 0.3ETVAdd consuming IntoIterator to lock_free_hashtable Summary: Add `IntoIter` structs and `IntoIterator` impls for `LockFreeRawTable` and `ShardedLockFreeRawTable`, enabling owned iteration that reclaims entries without cloning overhead. Key safety invariant: only the current (most recent) table owns entries. Previous tables store duplicate raw pointers and must not drop them — they just free their slot arrays via the `_prev` chain. Reviewed By: ndmitchell Differential Revision: D97811908 fbshipit-source-id: 28dd43101cbc9ee5b6fe86a0e0bbb565bf519349github.com-facebook-buck2 · 5f57df05 · 2026-03-23
- 0.3ETVAdd version helpers and refactor validate_version Summary: Shared version-format helpers for the release workflows: parse_semver, is_prerelease, and to_marketplace (VS Code Marketplace version mapping). Used as a CLI from GitHub Actions and as a library from internal release scripts. Refactors validate_version.py to delegate version parsing to parse_semver, eliminating duplicated regex and leading-zero validation. Reviewed By: rchen152 Differential Revision: D104757537 fbshipit-source-id: b8312e803aa8f450c3eb7a9990959e695cba674egithub.com-facebook-pyrefly · 38baa007 · 2026-05-12
- 0.3ETVwire resolver into DefaultConfigConfigurer Summary: This is the CLI half of "the same logic runs for both CLI and LSP". With this change, `pyrefly check` on an unconfigured project synthesizes a real `ConfigFile` via `resolve_unconfigured_config` instead of the historical IDE-without-config defaults. The wiring point is `DefaultConfigConfigurer` (and its `-WithOverrides` sibling) in `pyrefly/lib/commands/config_finder.rs`. Both configurers run the new helper `apply_unconfigured_resolver_if_applicable` before finalizing: - If `config.source` is `File(_)` (loaded from a real pyrefly.toml/pyproject.toml) or the config already carries a preset or synthesized_preset_reason, the helper is a no-op. Idempotent and safe to call repeatedly. - Otherwise the synthesized config is replaced with `resolve_unconfigured_config(root, Auto)`. We preserve the project layout the original synthesized config established (`import_root`, `fallback_search_path`, `source`) so search-path heuristics and project-root-aware behavior keep working. Migrated paths from mypy/pyright are absolutized against the root. This naturally changes the default behavior of `pyrefly check` on projects that have neither a `pyrefly.toml` nor a `[tool.pyrefly]`: previously close to the `default` preset, now `Basic` (or migrated from mypy/pyright when detected). The release-notes commit calls this out. Reviewed By: stroxler Differential Revision: D103653831 fbshipit-source-id: 70a2db9781c7b5da8e5d089184335c6d01a48011github.com-facebook-pyrefly · 191c2c2f · 2026-05-05
- 0.3ETVresolve_unconfigured_config(start, override) Summary: Builds on the in-memory migration helper from commit 1 to give CLI and LSP a single function for "produce a `ConfigFile` for a project root that has no `pyrefly.toml`/`[tool.pyrefly]`." The next two commits wire this into `DefaultConfigConfigurer` (CLI) and `WorkspaceConfigConfigurer` (LSP) so both code paths share identical synthesis behavior. The resolver has two modes: - An explicit override (`Off` | `Basic` | `Legacy` | `Default` | `Strict`) skips auto-detection and produces an empty `ConfigFile` carrying that preset and `synthesized_preset_reason = IdeOverride`. This is the path the IDE uses when the user has set `python.pyrefly.typeCheckingMode` to a specific preset; we do not second-guess them. - `Auto` runs `find_and_migrate_in_memory(start)` from commit 1. If mypy or pyright is detected and migrates cleanly, the migrated config (preset already set by the migrator) is stamped with `Migrated(kind)` carrying the `MigratedFromKind` returned by the helper. If nothing is found, falls back to `Preset::Basic` with `NoNearbyConfig`. `project_includes` is populated at construction time (`ConfigFile::default_project_includes()` for the IdeOverride and basic-fallback paths, plus a fallback for migrated configs that have empty includes), so the consumer (commit 3) doesn't need a post-hoc `.is_empty()` check. Migration *failure* (a malformed mypy.ini or pyrightconfig.json) is treated the same as "nothing found": logged at debug, fall back to basic. A broken nearby config must not prevent Pyrefly from running — the user's source files are still type-checkable. The new `UnconfiguredOverride` enum is the resolver's input. It maps 1:1 to the values the VS Code setting accepts, with `Auto` as the default when the setting is absent. Reviewed By: stroxler Differential Revision: D103653824 fbshipit-source-id: 50bdb48a80b02bda75ad8f72479b02fe7e356c86github.com-facebook-pyrefly · 6c8a5fcf · 2026-05-05
- 0.3ETVSolver: invert bound-absorb direction for upper bounds Summary: `add_upper_bound` was called multiple times on a TypeVar with bounds where one was a subtype of the other, and `get_new_bound` always kept the supertype. Given two recorded bounds `A` and `B` with `B <: A`, that is correct for lower bounds (from `A <: T` we get `B <: T` by transitivity, so keeping `A` carries strictly more information), but exactly backwards for upper bounds: from `T <: B` we get `T <: A` by transitivity, so for upper bounds `B` is the stricter constraint we want to keep. Concretely, when a call's return contained a TypeVar in multiple union arms and was checked against a union return hint, decomposing arm-by-arm could record both a tight bound like `T <: int` and a loose one like `T <: int | () -> int` for the same Var. The previous logic discarded the tight bound, the solver picked the loose union as `T`, and substituting back produced bogus types like `A[A[int]]` — surfacing to users as a baffling `bad-assignment` / `bad-return` error whose reported source type didn't appear anywhere in their code. `get_new_bound` now takes an `is_upper` flag and inverts its keep/replace decision when invoked from the upper-bound path. The lower-bound behavior is unchanged. A regression test in `pyrefly/lib/test/contextual.rs` constructs the minimal upper-bounds-only scenario and verifies the call type-checks cleanly; the test fails with the old logic and passes with the new. Reviewed By: rchen152 Differential Revision: D105764288 fbshipit-source-id: 948496a32e952ae8658f8a540883dc6e020e6af2github.com-facebook-pyrefly · 1abb1016 · 2026-05-20
- 0.3ETVExtract ClassFields newtype from ClassInner Summary: Extract the `SmallMap<Name, ClassFieldProperties>` stored in `ClassInner` into a `ClassFields` newtype with its own impl block. The field accessor methods on `Class` now delegate to `ClassFields`. This prepares for moving field storage out of `ClassInner` in a follow-up. Performance (median of 3 opt-clang-thinlto runs, baseline=parent of stack): PyTorch: | metric | baseline | this diff | cumulative | |--------|----------|-----------|------------| | cpu | 40.65s | 40.57s | -0.2% | | wall | 2.24s | 2.20s | -1.8% | | maxrss | 0.93GB | 0.93GB | +0.0% | Reviewed By: yangdanny97 Differential Revision: D96515739 fbshipit-source-id: 23fe73ba9162b5b6d28789757098345965f1bbbdgithub.com-facebook-pyrefly · 8632b83a · 2026-03-16
- 0.2ETVAvoid ArcId<ModuleDataMut> refcount ops in cross-module lookups Summary: Change `TransactionHandle::module_data` from owned `ArcId<ModuleDataMut>` to `&'a ArcId<ModuleDataMut>`, and propagate this through `Transaction::get_module`, `get_imported_module`, `get_module_ex`, `lookup`, and `lookup_answer`. Previously, every cross-module lookup (`LookupAnswer::get`) cloned the `ArcId<ModuleDataMut>` via `get_module` → `get_module_ex` (which called `.dupe()`) and dropped it at function exit. Under high parallelism, the `lock incq`/`lock decq` on the shared refcount caused severe cache line bouncing between cores — annotated assembly showed 40% of `LookupAnswer::get`'s self-time on the `lock decq` alone. Since the `ArcId<ModuleDataMut>` lives in the transaction's `LockedMap` for the duration of the transaction, we can borrow it instead of cloning. The fast path in `lookup_answer` only needs `&ArcId` (for `demand`, `get_answers`, `key_to_idx`, `get_idx`). The rare slow path (`solve_exported_key`) dupes at the point of need. This benefits all cross-module lookups (KeyClassMetadata, KeyClassMro, KeyClassField, KeyClassSynthesizedFields, etc.), not just any single key type. PyTorch (3 runs each, striped): Baseline: wall 2.57/2.55/2.55s, user 68.67/68.83/66.85s Optimized: wall 2.49/2.46/2.45s, user 62.47/63.68/62.40s Delta: -3.5% wall, -7.7% user CPU Reviewed By: ndmitchell Differential Revision: D96073937 fbshipit-source-id: 61bf99c110f144dc6685171eb60ba6739d92dec8github.com-facebook-pyrefly · dd0563bc · 2026-03-11
- 0.2ETVVS Code extension new setting + status bar redesign Summary: The client-side bookend to commits 5-7. Adds the new `python.pyrefly.typeCheckingMode` setting to the VSIX schema, opts the extension into the V2 wire shape for the typeErrorDisplayStatus request, and rewrites the status-bar renderer to handle both wire shapes. - `lsp/package.json`: - Adds `python.pyrefly.typeCheckingMode` (enum: auto / off / basic / legacy / default / strict; default `auto`). Description spells out that the setting only governs files not covered by a `pyrefly.toml` or `[tool.pyrefly]` section — those configurations always take precedence — and lists each preset's effect, so users discover the setting from the settings UI alone. - Marks `python.pyrefly.displayTypeErrors` deprecated via `deprecationMessage` (VS Code renders this as a strikethrough + notice). Property kept; the server still accepts it and maps it onto the new model. Description rewritten to point users at the replacement and document the legacy mapping. - Reworks the `disableLanguageServices` description so it points users at `typeCheckingMode` and `disableTypeErrors` instead of the deprecated `displayTypeErrors`. - `lsp/src/extension.ts`: - Always sends `typeErrorDisplayStatusVersion: "v2"` in init options. Server clamping (commit 7) means an old binary that doesn't know V2 still returns V1 — safe to declare unconditionally. - `lsp/src/status-bar.ts`: - Dispatch on response shape. `typeof resp === 'string'` → V1 renderer (kept verbatim from the previous implementation, with a new `no-config-file` case to match the current server's V1 output). Object response → switch on `resp.version`. Unknown future version → hide the status bar (defensive only — server clamping prevents this in practice). - V1 renderer's "no config file" tooltip continues to mention legacy `displayTypeErrors=force-on` (not the new settings) — V1 is the wire shape produced by older binaries that don't know `typeCheckingMode` or `disableTypeErrors`, so directing those users at the legacy setting is correct. - V2 renderer: status-bar text is `Pyrefly` when `label` is null, `Pyrefly (${label})` otherwise. Tooltip is the server-supplied markdown plus a trailing `[Docs](url)` link. The server now owns all wording; the client just renders it. - `lsp/README.md`: documents the new setting and the deprecated legacy one with the mapping table. Updated the "Features" bullet to describe the new behavior on a project without a Pyrefly configuration (basic preset or auto-migration). Reviewed By: kinto0 Differential Revision: D103653826 fbshipit-source-id: fc7889c97f2a0bb10ba661e4d324481dc864a573github.com-facebook-pyrefly · 8da06da5 · 2026-05-05
- 0.2ETVAdd lsp_interaction tests for EmptyResponseReason telemetry Summary: Context: This stack improves diagnostics for empty LSP navigation results (go-to-definition, hover, etc.). Previously, when the server returned null, we could not distinguish expected cases (user clicked on whitespace) from unexpected failures (module not found, internal bugs). These changes add structured EmptyResponseReason telemetry so we can identify and fix the root causes of failed navigations. This Diff: Add 6 lsp_interaction tests verifying that the correct EmptyResponseReason is set on telemetry events for various LSP scenarios: successful definition (no reason), LanguageServicesDisabled, MethodDisabled, NotAnIdentifier (click on comment), DefinitionNotFound (undefined name), and DefinitionNotFound with Attribute context (nonexistent attribute on int). Note: pyrefly does not collect any telemetry in the open-source build. These tests exercise the telemetry infrastructure code paths regardless. Reviewed By: grievejia Differential Revision: D98221993 fbshipit-source-id: 79f2b668b8df1ce41062390b42405485486e0d30github.com-facebook-pyrefly · be0c0620 · 2026-03-26