Skip to content

feat: PowerShell language support (.ps1/.psm1) - #58

Draft
qoole wants to merge 11 commits into
NanoNets:mainfrom
qoole:feat/powershell-support-v2
Draft

feat: PowerShell language support (.ps1/.psm1)#58
qoole wants to merge 11 commits into
NanoNets:mainfrom
qoole:feat/powershell-support-v2

Conversation

@qoole

@qoole qoole commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Builds on #40 (tree-sitter 0.25) — this branch is stacked on that PR's branch, so its two commits appear here until #40 merges; the PowerShell work is the five commits from chore: add PowerShell tree-sitter grammar onward. Opening as a draft until #40 lands, then I'll rebase onto main and mark it ready.

Tier-1 graph extraction for PowerShell via tree-sitter-powershell (airbus-cert; prebuilds for all six platform/arch combos — the reason #40's 0.25 runtime bump is the prerequisite).

What

  • Definitions: functions (incl. filter/workflow), classes, methods & constructors, enums. exported=true for top-level defs and class members; functions nested inside functions/methods are scope-local → false. (PowerShell has no file-local export syntax; Export-ModuleMember/manifest interpretation is cross-file and deferred — see limitations.)
  • Call edges: command invocations (scope-/module-qualifiers stripped: script:Foo, Mod\Get-Foo), member calls $obj.M(), static calls [Type]::M(), call-operator statics & Get-Foo. Dynamic invocation (& $var) produces no edge.
  • Import edges: dot-sourcing (. ./x.ps1, quoted paths, backslash-normalized), Import-Module with relative paths (module names stay external specifiers, like Go stdlib imports), using module (extracted from its command shape — the grammar has no dedicated node).
  • Case-insensitive resolution: PowerShell is case-insensitive; call/heritage/typed-member lookups case-fold within a PowerShell-only domain (a PS FOO can never link to a Python foo), original casing preserved everywhere, lookup order exact-local → folded-local → exact-global → folded-global (local scope wins, matching PS semantics). Folded matches surface as confidence: "inferred".
  • Typed-receiver bindings: typed params (incl. decorated [Parameter(...)][Widget]$w), typed properties, cast declarations, [Widget]::new(), New-Object (positional and -TypeName), $this → enclosing class.
  • Builtin-cmdlet denylist: calls to the core cmdlet/Pester-DSL surface (Get-ChildItem, Describe, …) never bind to a same-named in-repo definition (usually a Pester mock) — dropped-edge-is-safe, the same posture the resolver already takes for unresolvable member calls.
  • Ingest: UTF-16LE .ps1 (Windows PowerShell 5.1 Out-File default) decoded at the shared readSourceFile site (hash-what-you-parse holds); Pester *.Tests.ps1 classified as test files and de-ranked accordingly; .psd1 deliberately not graph-parsed (manifests yield no definitions).
  • Deep grammar import (tree-sitter-powershell/bindings/node/index.js) avoids a DEP0151 warning on every CLI run (package has extensionless main + no exports map).

Fit with recent main

This was originally built against pre-0.9.0 main and re-anchored after the recent landings; the seams were adapted rather than ported blind:

Evidence

  • Suite: 618/618 green on this branch (35 PowerShell-specific tests: full-pipeline fixtures modeled on the Go tests, bindings, case-collision isolation, malformed-input tolerance, UTF-16, CRLF, >32 KB files, parser alternation, test-path classification pinning).
  • Real-world battery (run on the pre-rebase branch; spot-revalidated post-rebase): 8 repos (Pester, ComputerManagementDsc, dbatools, ImportExcel, posh-git, PSFramework, Plaster, psake) — every build clean, double-build byte-identical determinism 8/8, incremental rebuild correct 8/8 (dbatools: 1,707 files, full 34 s / incremental 2.5 s).
  • Function recall vs an exhaustive regex census: 88–98% per repo, with every miss attributed to upstream grammar parse errors, none to extraction logic.
  • Resolution quality on dbatools: the most-called function (Stop-Function, 2,857 call sites, shadowed by 10 Pester mocks) resolves 726 incoming call edges to its production definition.
  • Import wiring on posh-git: 24/30 import edges resolve to in-repo files via literal $PSScriptRoot resolution (was 0/30); the 6 unresolved are genuinely dynamic (runtime variables) and stay raw by design. Re-verified on this exact branch post-rebase, including byte-identical cold-rebuild determinism.
  • Parse health (upstream grammar, not this integration): 41–94% of files parse error-free per repo; the dominant causes bisect to two airbus-cert grammar gaps (unquoted comma-separated bareword arguments; bare trailing scriptblock arguments). Extraction degrades gracefully — error-containing files still yield correct nodes and edges. Happy to file those upstream grammar issues.

Known limitations (deliberate)

  • Export-ModuleMember / manifest FunctionsToExport not interpreted (uniform exported=true instead) — the common unquoted multi-name idiom doesn't parse in the current grammar anyway.
  • $PSScriptRoot-interpolated, Join-Path-computed, and wildcard import arguments degrade to raw specifiers.
  • .psd1 manifest relationships (RootModule/NestedModules) not graphed.
  • Monorepo scope markers unchanged (PS has no fixed-name manifest file).
  • Class base lists conflate base class and interfaces as extends (PS cannot define interfaces; targets stay external names).

qoole and others added 11 commits August 5, 2026 17:48
Newer grammar releases in the tree-sitter ecosystem require the 0.25
runtime, so bump tree-sitter ^0.21.1→^0.25.0 and the Python/Go grammars
to ^0.25.0 to unlock languages whose grammars need it. tree-sitter-typescript
stays pinned at ^0.23.2 (still the latest release) — it declares
peerOptional tree-sitter@^0.21.0, so an npm `overrides` entry pins its
nested tree-sitter to the root one so the repo-root install resolves
cleanly; verified the grammar's native binding still loads and parses
correctly under tree-sitter 0.25.1.

Also updates the stale-parse-limit comment in extract.ts: the chunked
callback parse predates 0.25 (which lifted the 32KB string-parse limit)
and is kept because it's behavior-identical and exercised by existing
tests. No behavior change; full suite green (493/493).
Use stable syntax-node IDs to avoid duplicate reference edges and align the supported Node version with native Windows prebuilds.
Adds the PowerShell language to extract.ts: describePowerShell recognizes
function/class/enum/method statements, psCalleeName/psImportKind/
psImportSpecifier classify commands (dot-source, Import-Module, using
module) versus real calls, and psExported covers PowerShell's lack of a
file-local export marker. bindings.ts's FileBindings gains a case-insensitive
mode and PowerShell-specific defName/resolveRecvType handling ($this,
scope-qualified definition names).
…, builtin cmdlets

resolve.ts bundles the name-lookup and owner-index maps (NameIndex/
OwnerIndex) and adds a PowerShell case-folded tier alongside the existing
exact-match one: same-file exact wins first, then same-file case-folded,
then whole-repo exact, then whole-repo case-folded. A PowerShell caller
that isn't itself a test file demotes test-path candidates at the two
global tiers before the uniqueness check, so Pester's habit of
redeclaring a prod name across several *.Tests.ps1 files doesn't drop
every cross-file call as ambiguous. PS_BUILTIN_CMDLETS denylists the core
cmdlet/Pester-DSL surface so a call to a builtin never wires to an
unrelated in-repo definition of the same name. $PSScriptRoot resolves as
a literal for import paths, and PS_IMPORT_EXTS keeps PowerShell's module
extensions from cross-matching TS/Python specifiers.

isTestPath moves to its own src/util/testpath.ts (a hard graph-topology
input for resolve.ts now, not just ask.ts's soft rank-penalty); ask.ts
re-exports it for backward compatibility.
CODE_EXTENSIONS picks up .ps1/.psm1/.psd1; isTestPath recognizes
.tests.ps1 (Pester's naming convention). New test/graph-powershell.test.ts,
test/testpath.test.ts, and test/utf16-routing.test.ts cover PowerShell
graph extraction/resolution end-to-end and the UTF-16LE decode path.
Every existing test file touched by the case-folding, test-aware
tie-break, builtin-cmdlet denylist, and ingest changes is extended with
PowerShell-specific coverage alongside its existing cases.
@qoole

qoole commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Added a domain-isolation fix found while extending the language work: PowerShell method nodes were entering the shared member-owner index, so a typed member call from another language could in principle wire cross-language to a PS class method. PS methods (and PS heritage edges) now live only in their own indexes, mirroring how PS bare names were already isolated. Suite is 621/621 on the branch.

Assaf750 added a commit to Assaf750/Graft that referenced this pull request Aug 12, 2026
…and Rust behaviour

Records the verified starting point before any Rust work: PR NanoNets#40/NanoNets#58/NanoNets#59 heads
double-checked via the GitHub API and git ls-remote, strict-containment proof of
the stack, and the finding that the stack carries no upstream CI signal at all.

Suite on the pinned head is 638/645 with 3 failures. All three are an upstream
locale defect (bare toLocaleString() resolving to ar-SA digits), reproduced
verbatim on main and untouched by any Rust commit, so they are attributed
upstream rather than counted as a Rust regression. Fix is deliberately deferred
to its own commit.

Also records measured behaviour on a Cargo workspace fixture: what already
resolves, and the six gaps that Batches B-J exist to close — chiefly that
cross-crate symbol imports never resolve because module-path resolution maps
paths to files only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants