Sam Goldman
90d · built 2026-07-24
90-day totals
- Commits
- 79
- Grow
- 4.6
- Maintenance
- 6.5
- Fixes
- 1.2
- Total ETV
- 12.2
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 53 %
- By Growth share
- Top 82 %
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).
↓-67.9 %
vs 28 prior
↓-8.6 pp
recent vs prior
↓-18.9 pp
recent vs prior
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'.
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.9ETVWalk project files with the `ignore` crate Summary: Before this diff, we used `glob::glob` to walk the file system, using `read_dir` to manually recurse in a single thread. This diff replaces this traversal with the `ignore` crate, significantly improving performance. In addition to parallelism, the `ignore` also makes better use of filesystem APIs. Before, we issued a separate `stat` call per file via the `is_dir` check. The `ignore` crate uses the `d_type` field of the dirent struct, when available, avoiding the extra stat call. Lastly, this traversal avoids walking directories by checking the excludes filters during traversal. This helps a lot in practice, especially in the common case where a large `.venv` directory is in the project root. For workspace indexing, which has a file limit, we use a single-threaded, sorted walk to preserve deterministic results. Using a single thread is fine, since we stop the traversal after only 2000 results anyway. This diff changes error reporting in some cases, and could lead to errors for misconfigured projects which were silent before. For example, broken symlinks and symlink loops are now reported as failures, unless the symlink is excluded. Benchmark results show that parallel directory traversal is significantly more performant than `eden glob` for EdenFS file systems (common in Meta, uncommon in OSS). Benchmark Results: Paired AB/BA measurements from optimized binaries. Baseline is the trunk parent of `D108434328`; experiment is this diff, including `D108434328`. CPU is user+system time; deltas are experiment minus baseline. Confidence intervals are 95% bootstrap CIs over paired deltas. Positive deltas are regressions. Max concurrency of 64 threads. | workload | pairs | CPU delta | CPU 95% CI | wall delta | wall 95% CI | | `homeassistant-core` | 10 | +2.112s (+1.38%) | [+0.971, +3.118]s | -2.122s (-35.66%) | [-2.249, -1.954]s | | `transformers` | 10 | +5.437s (+3.51%) | [-0.296, +10.752]s | -0.042s (-0.87%) | [-0.149, +0.056]s | | `tensorflow` | 15 | +0.336s (+0.52%) | [-1.501, +2.093]s | -0.201s (-8.79%) | [-0.321, -0.075]s | | `pandas-stubs` | 15 | -0.183s (-0.44%) | [-0.731, +0.319]s | -0.093s (-4.33%) | [-0.225, +0.005]s | | `pytorch` | 15 | -1.495s (-2.93%) | [-2.012, -0.957]s | -0.195s (-11.45%) | [-0.232, -0.159]s | | `airflow` | 15 | +0.131s (+0.19%) | [-1.543, +1.800]s | -0.190s (-8.15%) | [-0.307, -0.087]s | | `prefect` | 15 | -4.295s (-6.82%) | [-5.857, -2.675]s | -3.797s (-64.36%) | [-3.983, -3.646]s | | `open-webui` | 15 | -0.117s (-0.70%) | [-0.375, +0.147]s | +0.005s (+0.20%) | [-0.050, +0.064]s | | `numpy` | 40 | -0.085s (-1.12%) | [-0.205, +0.035]s | -0.051s (-7.23%) | [-0.085, -0.022]s | | `ray` | 40 | -0.014s (-0.22%) | [-0.057, +0.030]s | -0.061s (-8.54%) | [-0.095, -0.028]s | | `scipy` | 40 | -0.194s (-2.96%) | [-0.235, -0.150]s | -0.036s (-4.34%) | [-0.071, +0.003]s | | `sqlalchemy` | 40 | -0.080s (-0.89%) | [-0.137, -0.022]s | -0.041s (-5.82%) | [-0.065, -0.019]s | | `sympy` | 40 | -0.097s (-1.27%) | [-0.142, -0.052]s | -0.079s (-12.58%) | [-0.120, -0.043]s | | `small-suite` | 40 | -0.522s (-1.41%) | [-0.702, -0.352]s | -0.262s (-7.64%) | [-0.380, -0.145]s | Reviewed By: connernilsen Differential Revision: D108438904 fbshipit-source-id: 3eb3c4abff5d470526ad468c6c2be30998cb5d4egithub.com-facebook-pyrefly · 33fba019 · 2026-06-13
- 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.8ETVLazy materialize implicit builtins Summary: Eagerly adding the full `builtins` and `__builtins__` wildcard set to every module gives every file hundreds of retained `Key::Import` and `Binding::Import` rows even though most files read only a small subset. This changes name lookup so a true module-scope miss checks the shared builtin wildcard exports and materializes only the builtin names that are actually read, using the same path for full checks and LSP. The lazy static entry is still represented as an import binding after the binding pass, so downstream consumers that inspect bindings can resolve it, while export collection and special-export lookup preserve shadowing and avoid treating fallback builtins as real module definitions. Flow merging treats these implicit builtins as defined so a first use inside one branch does not become a conditional local. Benchmark Results: Paired AB/BA measurements from optimized binaries. Baseline is `D108438904`, the ignore-based file traversal diff; experiment is this diff grafted on top. CPU is user+system time; deltas are experiment minus baseline. MaxRSS is maximum resident set size from `/usr/bin/time -v`. Confidence intervals are 95% bootstrap CIs over paired deltas. Positive deltas are regressions. Max concurrency of 64 threads. | workload | pairs | CPU delta | CPU 95% CI | wall delta | wall 95% CI | MaxRSS delta | MaxRSS 95% CI | | `homeassistant-core` | 10 | -16.652s (-10.74%) | [-18.029, -15.460]s | -0.322s (-8.39%) | [-0.423, -0.226]s | -255.0 MB (-12.93%) | [-267.2, -242.1] MB | | `transformers` | 10 | -4.868s (-3.05%) | [-13.248, +3.296]s | -0.049s (-1.02%) | [-0.247, +0.140]s | -117.9 MB (-7.07%) | [-132.7, -102.9] MB | | `tensorflow` | 15 | -3.877s (-5.85%) | [-6.256, -1.321]s | -0.073s (-3.58%) | [-0.179, +0.042]s | -51.2 MB (-7.06%) | [-61.1, -41.7] MB | | `pandas-stubs` | 15 | -1.653s (-4.03%) | [-2.091, -1.207]s | -0.029s (-1.35%) | [-0.127, +0.079]s | -24.8 MB (-3.72%) | [-27.3, -22.4] MB | | `pytorch` | 15 | -3.239s (-6.48%) | [-3.647, -2.823]s | -0.023s (-1.55%) | [-0.081, +0.050]s | -41.1 MB (-3.85%) | [-49.9, -32.6] MB | | `airflow` | 15 | -3.277s (-4.72%) | [-4.174, -2.482]s | -0.004s (-0.19%) | [-0.076, +0.082]s | -91.9 MB (-9.90%) | [-96.4, -87.5] MB | | `prefect` | 15 | +0.087s (+0.14%) | [-1.715, +1.932]s | +0.027s (+1.21%) | [-0.056, +0.104]s | -80.0 MB (-6.18%) | [-83.1, -76.8] MB | | `open-webui` | 15 | -1.891s (-11.25%) | [-2.633, -1.302]s | -0.099s (-3.65%) | [-0.240, +0.024]s | -62.1 MB (-4.76%) | [-68.8, -55.5] MB | | `numpy` | 40 | -1.469s (-19.11%) | [-1.581, -1.358]s | -0.030s (-4.04%) | [-0.073, +0.014]s | -18.8 MB (-5.04%) | [-21.3, -16.3] MB | | `ray` | 40 | -1.755s (-27.77%) | [-1.796, -1.713]s | -0.079s (-13.21%) | [-0.111, -0.050]s | -27.3 MB (-7.92%) | [-29.8, -24.7] MB | | `scipy` | 40 | -1.827s (-28.84%) | [-1.862, -1.791]s | -0.056s (-8.15%) | [-0.078, -0.034]s | -30.6 MB (-8.68%) | [-32.8, -28.3] MB | | `sqlalchemy` | 40 | -1.647s (-18.88%) | [-1.720, -1.576]s | -0.078s (-11.83%) | [-0.096, -0.059]s | -21.4 MB (-5.51%) | [-24.4, -18.4] MB | | `sympy` | 40 | -2.111s (-27.62%) | [-2.160, -2.054]s | -0.068s (-12.05%) | [-0.105, -0.034]s | -30.9 MB (-7.18%) | [-34.6, -27.4] MB | | `small-suite` | 40 | -8.917s (-24.12%) | [-9.123, -8.722]s | -0.291s (-8.87%) | [-0.397, -0.199]s | -30.8 MB (-7.16%) | [-34.4, -27.1] MB | Reviewed By: kinto0 Differential Revision: D108495967 fbshipit-source-id: 83126b252defbc766fb5c829fc76dfcd2843a420github.com-facebook-pyrefly · 86f0da71 · 2026-06-16
- 0.7ETVSkip receiver parameters in variance inference Summary: Receiver annotations on methods were counted as ordinary contravariant parameters when inferring class variance, which made classes like `Container[T]` invariant even though method lookup either binds the receiver dynamically or requantifies it for class access. This teaches variance inference to use Pyrefly's existing class-field method/property classification and skip only receiver-bound first parameters, preserving normal callable fields and staticmethods. Reviewed By: rchen152 Differential Revision: D108473085 fbshipit-source-id: b75b61eca968d62f06fcc35efe175b8374cdc221github.com-facebook-pyrefly · c4e212d3 · 2026-06-15
- 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 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.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.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.3ETVOptimize error rendering and output Summary: Testing Pyrefly on homeassistant-core, I noticed that printing errors took almost 1s (36,762 errors). There are two interesting cases: 1. Printing to an interactive terminal, where we print colorized output 2. Non-interactive output (including agentic use) which is no-color We use the `anstream` crate to detect what kind of output we are doing, which is standard and correct. Problems: 1. We always produced colorized diagnostics, including colorized snippets via `ruff_annotate_snippets`, even in no-color mode. In this mode, the ANSI escape sequences needed to be stripped out by `anstream`'s "StripStream" wrapper. 2. We did not `lock()` the the output stream, so every write needed to lock/unlock 3. We did not buffer our writes. `stdout` is line-buffered by default, and our error format includes lots of newlines per error. This diff addresses all of the above. We introduce a "ErrorRenderer" abstraction, which uses the auto-detected output format (color or plain). In plain mode, we do not create colorized output with ANSI escape sequences. We also lock the output stream and add buffering. The code flushes the buffer after each error. I also tested flushing only at the end, but there was no difference. Both were significantly better compared to line-buffered behavior. I also migrated some test code over to the new ErrorRenderer API. These changes should introduce no functional difference, but made it possible to remove some public APIs which were inefficient due to per-error setup, which we want to avoid. Results (data is a fairly noisy): * Plain output: wall time avg 6.905s -> 6.092s, printing time 1.036s -> 0.251s * Color output: wall time avg 6.764s -> 6.517s, printing time 0.934s -> 0.641s Reviewed By: rchen152 Differential Revision: D107294970 fbshipit-source-id: 69b99de9ae82218878e8da2c5914e3c8ff3169eagithub.com-facebook-pyrefly · ad4bbe3a · 2026-06-02
- 0.3ETVMove calculation results out of the mutex Summary: The completed calculation path was holding the calculation mutex while cloning the cached `Arc` result. Perf on `transformers` showed `queued_spin_lock_slowpath` under `Calculation<Arc<ClassMetadata>>::get`, so the `Arc` refcount increment was part of the contended critical section. Store the final result in a once-written `UnsafeCell<MaybeUninit<T>>` outside the mutex state, and use the terminal `Calculated` status as the initialization marker. Splitting the calculating-thread set out of the status enum keeps `Calculation<Arc<_>>` at the old 40-byte layout while allowing `get`, `record_value`, and `write_unlock` to drop the mutex before cloning the final value. `Calculation::drop` uses `Mutex::get_mut` to inspect the terminal status through exclusive access, so it does not acquire the mutex just to decide whether the result cell is initialized. Benchmark Results: Paired AB/BA measurements from optimized binaries. CPU is user+system time; deltas are experiment minus baseline. Confidence intervals are 95% bootstrap CIs over paired deltas. Positive deltas are regressions. Max concurrency of 64 threads. | workload | pairs | CPU delta | CPU 95% CI | wall delta | wall 95% CI | | `homeassistant-core` | 10 | -29.913s (-16.27%) | [-31.613, -28.101]s | -0.555s (-8.51%) | [-0.593, -0.521]s | | `transformers` | 10 | -21.859s (-12.48%) | [-26.877, -16.952]s | -0.394s (-7.58%) | [-0.462, -0.344]s | | `tensorflow` | 15 | -11.452s (-14.51%) | [-13.687, -9.145]s | -0.119s (-4.77%) | [-0.263, +0.115]s | | `pandas-stubs` | 15 | -2.061s (-4.73%) | [-2.615, -1.528]s | -0.056s (-2.55%) | [-0.084, -0.029]s | | `pytorch` | 15 | -0.997s (-1.91%) | [-1.427, -0.557]s | -0.023s (-1.39%) | [-0.096, +0.027]s | | `airflow` | 15 | -0.313s (-0.46%) | [-1.301, +0.845]s | -0.094s (-4.02%) | [-0.227, -0.006]s | | `prefect` | 15 | -0.859s (-1.35%) | [-2.239, +0.620]s | -0.052s (-0.88%) | [-0.160, +0.051]s | | `open-webui` | 15 | -0.288s (-1.66%) | [-0.567, -0.024]s | +0.017s (+0.63%) | [-0.060, +0.099]s | | `numpy` | 40 | +0.167s (+2.24%) | [+0.062, +0.270]s | +0.029s (+4.26%) | [+0.001, +0.065]s | | `ray` | 40 | +0.088s (+1.44%) | [+0.049, +0.126]s | +0.012s (+1.94%) | [-0.005, +0.028]s | | `scipy` | 40 | +0.111s (+1.74%) | [+0.070, +0.153]s | +0.004s (+0.51%) | [-0.013, +0.021]s | | `sqlalchemy` | 40 | -0.048s (-0.54%) | [-0.122, +0.028]s | -0.003s (-0.41%) | [-0.016, +0.011]s | | `sympy` | 40 | +0.134s (+1.78%) | [+0.089, +0.178]s | +0.008s (+1.29%) | [-0.004, +0.019]s | Reviewed By: ndmitchell Differential Revision: D108335589 fbshipit-source-id: 70f14442a3d64477ae08d8031a61899f60726bcbgithub.com-facebook-pyrefly · 5f58f8ef · 2026-06-12
- 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.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.2ETVUnderstand `$MYPY_CONFIG_FILE_DIR` when migrating mypy configs Summary: mypy lets path-valued options reference the config file's own directory via the `$MYPY_CONFIG_FILE_DIR` variable — e.g. Airflow's `pyproject.toml` uses `mypy_path = ["$MYPY_CONFIG_FILE_DIR/airflow-core/src", ...]`. Pyrefly's mypy migration did not understand the variable and copied it verbatim into the migrated config, leaving every search path and include that used it broken. mypy expands `$MYPY_CONFIG_FILE_DIR` to the config file's directory, and pyrefly anchors a migrated config at that same directory and resolves its relative paths against it — so the variable denotes exactly the config root. Migration now strips a leading `$MYPY_CONFIG_FILE_DIR` from the path-valued options mypy itself path-expands (`mypy_path`, `files`, `python_executable`), yielding an equivalent relative path that stays portable (checked-in-able) rather than an absolute one. `exclude` (a regex) and non-path options are deliberately left untouched. Because both `pyrefly init` and the in-memory migration used when running with no `pyrefly.toml` resolve a migrated config's relative paths against the config's own directory, the same stripping is correct in both modes. Reviewed By: rchen152 Differential Revision: D108210962 fbshipit-source-id: 77e12d51defc7efb9975b903652b20a0dce77a9bgithub.com-facebook-pyrefly · f5fc1732 · 2026-06-11
- 0.2ETVDecouple annotated returns from implementation checks Summary: Published function signatures with explicit return annotations should not depend on implicit-return inference from their bodies. That dependency creates unnecessary answer SCCs and forces recursive or imported bodies during otherwise lazy signature lookup. Derive the published return directly from the annotation and move implicit-return validation into a separate expectation binding. Full-module checking still forces the expectation and preserves diagnostics, while signature consumers stop at the annotation. Reviewed By: yangdanny97 Differential Revision: D112836504 fbshipit-source-id: 5a68c1c17d6e66d9f6e3d14919b8120ffc3693c0github.com-facebook-pyrefly · 51c4ac7a · 2026-07-23
- 0.2ETVAdd LSP go-to-definition tests for `from m import name` fallback paths Summary: Add three regression tests in `lib/test/lsp/definition.rs` that lock in current navigation behavior for `from m import name` cases that resolve via fallback rather than an explicit export: - `import_via_module_getattr_test`: `m` defines a module-level `__getattr__`. Cursor on `name` should land on `__getattr__` in `m`. - `import_via_reexported_module_getattr_test`: `m` re-exports `__getattr__` from another module. Cursor on `name` should follow the re-export chain to the original `__getattr__`. - `import_submodule_via_from_test`: `m` is a package and `name` is a submodule (no explicit re-export from `m/__init__.py`). Cursor on `name` should land on `m/name.py`. These cases are about to be re-implemented later in the stack as the bind-time `Binding::ImportViaGetattr` and `Binding::Module(submodule)` shortcuts are replaced by solve-time / IDE-time fallbacks. Establishing the tests now means the subsequent diffs demonstrate behavior preservation rather than introducing it. Reviewed By: kinto0 Differential Revision: D103422582 fbshipit-source-id: 6d4c614b6ed9cf5f4290df953f8f6a3da6d3587fgithub.com-facebook-pyrefly · de26e9a0 · 2026-05-04
- 0.2ETVLSP synthesized configs use the resolver Summary: Wires `WorkspaceConfigConfigurer` through the same `apply_unconfigured_resolver_if_applicable` helper the CLI configurers use (commit 3). The override is read from the workspace's `type_checking_mode` (commit 5) and converted to `UnconfiguredOverride` via a new `From` impl on the LSP enum. To make the helper reusable across crates, it's promoted to `pub(crate)` in `pyrefly/lib/commands/config_finder.rs` and now takes the override as an explicit argument instead of hardcoding `Auto`. Note on Basic preset and IDE features: `Preset::Basic` itself sets `check_unannotated_defs: false` and `infer_return_types: Never`. The binding step keeps walking unannotated bodies in IDE mode anyway (D97137944, gated on `Require::keep_index()`), so hover / goto-def / find-references inside an unannotated body still work. What does NOT work under Basic is *return-type inference* — the function's signature stays `() -> Any` and inlay hints rendering the inferred return type have nothing to display. That's an intentional property of Basic ("low-noise baseline"); users who want full IDE features without committing to a `pyrefly.toml` should set `python.pyrefly.typeCheckingMode` to `"default"`. The pre-existing IDE-feature tests have been opted into Default in commit 6a so they continue to pass through this commit. Test churn: dynamic-switching tests in `lsp_interaction/configuration.rs` (`test_disable_type_errors_*` and `test_diagnostics_*`) previously did synchronous pull-based `diagnostic` calls right after a `did_change_configuration`. The new resolver wiring routes config changes through async config-cache invalidation, which races the synchronous pull. Those tests now wait on `publishDiagnostics` push notifications instead, which fire after the recheck completes. The dynamic switch coverage (empty → force-on → force-off, including the observable transitions in both directions) is preserved. Reviewed By: grievejia Differential Revision: D103653832 fbshipit-source-id: b45d9838db06d6ca555af8a3a27e1f1d8a45ada6github.com-facebook-pyrefly · 4e2a831c · 2026-05-05