Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ jobs:
npm test
npm run package
unzip -l vscode-sharpts-*.vsix | grep 'extension/bin/server/SharpTS.DebugAdapter.dll'
unzip -l vscode-sharpts-*.vsix | grep 'extension/bin/server/SharpTS.LanguageServer.dll'

# Discover and type-check/compile every cross-runtime workload through the
# real runner. This catches repository moves and declaration drift without
Expand Down Expand Up @@ -207,6 +208,59 @@ jobs:
shell: pwsh
run: ./scripts/test-packaged-sdk.ps1

editor-contract:
needs: workflow-policy
if: needs.workflow-policy.outputs.mode == 'full'
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-2025]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Setup pinned Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: tools/formatter-interop/package-lock.json
- name: Acquire integration dependencies
shell: pwsh
run: |
dotnet restore src/SharpTS.LanguageServer/SharpTS.LanguageServer.csproj
if ($LASTEXITCODE -ne 0) { throw 'Language-server restore failed.' }
dotnet restore tests/fixtures/FormatterInterop/SharpTS.FormatterInteropEvidence.csproj
if ($LASTEXITCODE -ne 0) { throw 'Formatter evidence restore failed.' }
npm ci --prefix tools/formatter-interop --ignore-scripts --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw 'Pinned formatter acquisition failed.' }
timeout-minutes: 5
- name: Verify protocol, formatter and installed tool contracts
shell: pwsh
run: ./scripts/test-editor-contract.ps1 -NoRestore -NoFormatterInstall
timeout-minutes: 25
- name: Upload editor contract evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: editor-contract-${{ matrix.os }}
if-no-files-found: ignore
path: |
artifacts/editor-contract/
artifacts/analysis-smoke/
artifacts/semantic-hover-smoke/
artifacts/semantic-completion-smoke/
artifacts/semantic-signature-smoke/
artifacts/member-reference-smoke/
artifacts/private-rename-smoke/
retention-days: 7

standalone:
needs: workflow-policy
if: needs.workflow-policy.outputs.mode == 'full'
Expand Down Expand Up @@ -588,7 +642,7 @@ jobs:
gate:
name: Gate
if: always()
needs: [workflow-policy, build, typescript-conformance, standalone, dap-macos-smoke, aot-ratchet, native-aot-compile-smoke, lightweight-validation]
needs: [workflow-policy, build, typescript-conformance, editor-contract, standalone, dap-macos-smoke, aot-ratchet, native-aot-compile-smoke, lightweight-validation]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
Expand All @@ -603,7 +657,7 @@ jobs:
throw "CI change classification or workflow policy did not pass."
}

$heavy = @('build', 'typescript-conformance', 'standalone', 'dap-macos-smoke', 'aot-ratchet', 'native-aot-compile-smoke')
$heavy = @('build', 'typescript-conformance', 'editor-contract', 'standalone', 'dap-macos-smoke', 'aot-ratchet', 'native-aot-compile-smoke')
if ($env:CHANGE_MODE -eq 'full') {
$bad = @($heavy | Where-Object { $needs.PSObject.Properties[$_].Value.result -ne 'success' })
if ($needs.'lightweight-validation'.result -ne 'skipped') {
Expand Down
153 changes: 153 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,159 @@ User code imports only public specifiers. `primitive:` modules are private imple
The user-facing declaration, interpreter export, and compiled emitter for a built-in must describe
the same surface. See [`src/SharpTS/stdlib/CONTRIBUTING.md`](src/SharpTS/stdlib/CONTRIBUTING.md).

## Editor analysis

`SharpTS.LanguageServer.Services.SemanticAnalysisService` owns completed editor analyses.
`DocumentStore.TryCapture` supplies one immutable open-buffer capture. The analysis key includes
that capture's versions/text, workspace roots, and service generation; each build records exact
compiler filesystem reads, probes, and inventories through the scoped `CompilerFileSystem` seam.
This includes negative import/configuration probes and disconnected configured roots, since a
closed root can become a reverse importer. Physical validation and CLR metadata generation checks
run before publication, reuse, and the final handler response. Lifecycle and file/folder
notifications invalidate the service and cancel obsolete builds.

A completed `AnalysisSnapshot` owns source documents, tokens, recovered parser output, a private
TypeMap, frozen bindings/member origins, diagnostics, options, and graph/completeness facts. No checker or resolver
escapes the builder. Published ASTs are read-only inputs and must never be checked again. Embedded
standard-library ASTs remain shared read-only compiler inputs. Cursor-specific recovery must use
separate artifacts keyed by caret/query context and policy, never mutate a base snapshot.
Recovered syntax and partial semantics are explicit; recovered configured roots keep lexical
rename completeness false. Workspace expansion can reuse an already checked source component
without inserting a closed declaration into the open-buffer overlay.

Editor parses opt into a flat, document-owned `EditorSyntaxIndex` over the existing AST references.
Its immutable ranges distinguish written names/expressions/types, grouping/header/body views,
source-equivalent replacements, generated syntax, and recovered syntax. Invocation metadata
captures the actual parentheses and top-level commas during parsing. Lookup uses deterministic
range/role ordering; TSX lowering retains original source ownership without turning generated
factory calls into written invocations. Ordinary compiler parsing keeps this index disabled.
The [syntax coverage table](docs/editor-syntax-coverage.md) defines supported views and limits.

Editor checking opts into a generation-owned `MemberIndex`, independent of the lexical
`BindingIndex`. Reference-equal source class owners and captured documents identify canonical
instance/static/private member groups. Actual checker declaration IDs alias that source owner
across preparatory passes; generic instantiation and inherited lookup retain its origin. Overloads and
getter/setter pairs share their source group. Parameter-properties retain separate lexical and
property identities. Lookup hooks record the member selected by the checker, including operation
facts and finite candidate sets. An aggregate is authoritative only when every required selection
is supported and agrees on one origin. Structural, dynamic, CLR/built-in and index-signature
fallbacks do not acquire fabricated source identities.

`FrozenMemberIndex` copies completed identities and occurrences into the same analysis snapshot,
without retaining the checker. Ordinary compiler checking leaves member capture disabled.
Current compound/logical and literal-index paths that fall back to `any` without selecting a
named source member remain unavailable; checked update operands can retain a proven read without
claiming write proof. This metadata does not change checker diagnostics or class compatibility.
Every new member identity denies rename. Lexical parameter-property bindings also deny rename,
independently of graph completeness, until lexical and property edits can be coordinated.
The [member provenance contract](docs/editor-member-provenance.md) records supported operations
and checker paths that currently cannot establish a source origin.

Full-mode `DefinitionService` selects lexical bindings first and only falls back to complete,
unambiguous frozen member origins when no lexical facet is selected. It projects canonical
declaration names through each target document's own line index, deduplicates full path/span
locations and orders results deterministically. Known definitions do not require an exhaustive
workspace graph. The existing request guard and analysis validation still refuse stale results.

Full-mode member references use the same lexical-first selection, then join fresh frozen member
groups across configured projects using normalized declaration paths/full name spans and exact
member facets. They require complete initialized workspace discovery before collecting any new
member locations, project each through its own captured source and deduplicate full spans. Graph
completion does not prove unsupported or unvisited member uses. Generation-local IDs remain
within one snapshot, and member reference results never grant general rename eligibility.
If a configured component's exact resolver-cache lookup misses a Windows URI drive spelling,
the builder selects only one equivalent already-loaded physical source module. Virtual module
names keep their existing identity rules; multiple physical candidates make discovery incomplete.

Full-mode `PrivateRenameService` uses a separate snapshot-local private-domain proof. The frozen
member index audits every original private token in one authoritative class span and requires
exact checker-owned declarations/occurrences plus entered-body evidence from the editor index.
Unresolved tokens, unvisited bodies, nested class environments, unsupported declaration groups
and target-document parse errors refuse the complete operation. Workspace discovery completeness
is independent of this single-file proof. The handler requires negotiated versioned document
changes and an exact open capture, returns the entire private token/placeholder during prepare,
and publishes one versioned document edit only after source/dependency/metadata validation.
The general member/reference rename gate stays false; no parser/checker language expansion or
spelling-based occurrence search is added.

`WithEditorMetadata` additionally captures actual declaration/occurrence types, source-linked
checker scopes, receiver member projections and invocation candidates/selections. Publication
in `FrozenEditorSemanticIndex` replaces raw types with bounded TypeScript presentations and
copies identities/collections; no type environment, mutable class or inference callback escapes.
Failed attempts clear stale results. Recovery, holes and incomplete candidates cannot establish
an overload winner. Source scopes preserve unavailable shadowing locals and canonical import
identities. Projections inspect already checked types without another inference pass, and
ordinary checking leaves capture disabled. The [semantic query contract](docs/editor-semantic-queries.md)
defines authority, unsupported paths and presentation limits.

Full-mode `SemanticHoverService` selects exact written declaration, occurrence, annotation and
member facts from that shared snapshot. Type-use capture records existing resolution decisions;
unresolved named fallbacks remain unavailable even when the compiler represents them as `any`.
Bounded callable presentation uses the public overload surface retained during checking, including
callables nested in other types. Private calls, assignments, brand checks and `super` use separate
name/receiver facts rather than an enclosing expression's result. Existing GUI/decorator/CLR hover
keeps precedence; full-mode CLR usage shares the checked analysis, while interop-only does not
request ordinary semantic analysis. The handler negotiates Markdown/plain text and revalidates
the captured state before returning its exact UTF-16 source range.

Full-mode `SemanticSignatureHelpService` joins parser-owned argument lists to frozen invocation
facts from the same checked graph. Exact signature construction, source owners, explicit type
arguments and callee/receiver annotation proof determine candidate eligibility. Existing signature
substitution carries that proof by reference; unresolved fallbacks cannot create displayed `any`
parameters. The consumer preserves public candidate order and only publishes a complete checker
selection. Parser commas and bounded signature ranges supply active parameters and negotiated
UTF-16 label offsets. Unfinished calls use the shared fresh cursor graph; unvisited inner calls
remain unavailable. Decorator help retains priority in both modes.

`Parser.ParseForEditor` clones the source capture for bounded cursor-local missing member names
and unfinished call/new lists, including a hidden name after `super.`. A repair gap must contain
the caret between consumed source and the next real token. Comma-run lookahead stops beyond the
remaining repair budget and polls cancellation. It preserves original offsets and marks holes
and recovered delimiters as non-authoritative. Bare `this.#` lexical failure and arbitrary broken
enclosing braces remain unavailable. The parser itself never checks or adds bindings.

Semantic cursor analysis uses an opt-in exact `ModuleResolver.EditorParseTarget` to create a fresh
target parse inside a fresh checked graph. The original lexer retains reference directives and
JSX pragmas; ordinary configuration, resolution, imports and program/library processing continue.
Other source modules parse normally, and embedded read-only library declarations remain on their
existing shared path. A configured cursor build refuses default-option fallback if configuration
loading or membership fails, or checking cannot produce a model. Published base graphs and cached
parse-only artifacts are never checked again.

Seeded builds require the same service owner, exact originating request stamp and target path,
plus current inputs. They replay captured reads, probes and inventories before reconstructing
the program, observe extra inputs, capture their own fresh metadata view and validate both seed
and combined inputs before publication. Missing or mismatched seeds use a fresh configured build.
Checked cursor entries share the ordinary admission/LRU/64 MiB estimated budget below, with an
additional four-entry cursor cap and no ordinary document aliases. Opaque weak-table identities
in their keys do not retain a base AST; the shared build owns its seed lease only until completion.

The existing parse-only syntax cache remains independent of checked cursor graphs.
`SemanticAnalysisService` keys those artifacts by base document identity, cursor, query and
recovery policy. Its separate LRU retains at most 32 artifacts and 8 MiB of estimated payload
within the combined byte budget; invalidation clears both syntax artifacts and checked analyses.

Identical in-flight requests coalesce. Caller cancellation stops only that caller's wait;
invalidation/disposal owns build cancellation. At most two builds execute and sixteen builds are
admitted concurrently. Completed entries use LRU eviction with an eight-entry bound and a 64 MiB
estimated source/semantic payload budget. This estimate is not a total heap bound: it excludes CLR
assembly image ownership and approximates AST/binding overhead. Active leases survive eviction;
cache/lease disposal releases metadata generations when their last owner finishes. Shared build
failures remain observable on stderr even if every caller has cancelled.

`AnalysisMetadataProvider` captures read-only project/manifest inputs and reference-counted
`MetadataLoadContext` generations. Captured private assembly bytes permit replacing user DLLs
while older requests finish; content fingerprints detect same-size/timestamp changes. XML docs
and decorator type-name caches validate against their own content/generation. Query-time refresh
does not run restore or load user assemblies into the execution runtime. The installed default
framework is a fixed host input; an explicit SDK directory is a mutable observed input.

Interop editor services use these metadata views. General checker CLR synthesis still follows
the runtime registry path: routing metadata-only Types through synthesis requires metadata-safe
attribute/primitive inspection and scoped synthesis identities. Missing custom-reference general
symbols remain partial and acquire no guessed source binding. The shared analysis service
conservatively invalidates on metadata changes without claiming that synthesis bridge.

## Interpreter architecture

The interpreter evaluates expressions to `RuntimeValue` and executes statements against a
Expand Down
15 changes: 12 additions & 3 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,22 @@ See [.NET types](docs/dotnet-types.md), [.NET integration](docs/dotnet-integrati
| TypeScript-source PDBs | ✅ | Portable PDBs with source documents, sequence points, locals/scopes, and async metadata. |
| Interpreter debug adapter | ✅ | Standalone DAP tool with cooperative breakpoints, stepping, inspection, exceptions, and worker threads. |
| VS Code extension | ✅ | Diagnostics, interop IntelliSense, and separate interpreted and compiled debug commands. |
| Standalone language server | ✅ | Diagnostics, navigation, references, safe rename domains, completion, hover, signatures, and quick fixes. |
| General property/member rename | Deferred | Refused when workspace completeness cannot make the edit safe. |
| Standalone language server | ✅ | Full-mode checked hover, lexical/member completion, call/new signatures, source definitions/references, lexical rename and a bounded ECMAScript `#private` rename domain; interop features in both modes. |
| General property/member rename | Deferred | Public/protected/TypeScript-private, structural and parameter-property rename remains unavailable, including in complete workspaces. |
| Editor formatting | ✅ | External pinned Prettier 3.9.9; SharpTS advertises no formatting provider. Real TS/TSX save checks are recorded. |
| Editor contract validation | ✅ | Six real-stdio suites, formatter evidence and exact installed-tool checks share an Ubuntu/Windows CI aggregate. Shipping VS Code activation, native Neovim full/two-server editing and native Helix full-mode editing have separate recorded evidence. |

See the [GUI overview](docs/gui/README.md), [language server guide](docs/language-server.md), and the
[interpreted](docs/debugging-interpreter.md) and [compiled](docs/debugging-typescript.md) debugging
guides.

For a sole TypeScript server, use `sharpts-lsp --language-features full --diagnostics all`.
For coexistence, use `interop-only --diagnostics sharpts-only`. CLI defaults remain independently
`full` features and `sharpts-only` diagnostics; diagnostics can change live, while feature mode is
fixed at initialization. The [verified editor matrix](docs/language-server.md#verified-editor-arrangements)
records the actual clients, pinned adapter and capability limits. These supported domains do not
claim TypeScript language-service parity.

## 7. Conformance baselines

Both corpora use committed path-by-path baselines. Percentages below are recalculated from the
Expand Down Expand Up @@ -163,7 +172,7 @@ not freeze historical benchmark magnitudes.
later than `tsc` reports it.
- A compiled dynamic-index miss on a plain object can produce `null` rather than `undefined`.
- Compiled indirect `eval` cannot access compiled local variables.
- The `Function` constructor is absent.
- General source construction through `Function` is unsupported; only the bounded forms documented above work.
- Compiled .NET interop propagates raw CLR exceptions rather than applying the interpreter's
JavaScript error-name mapping.
- `AsyncLocalStorage.run`/`exit` do not yet forward optional trailing arguments through the
Expand Down
Loading
Loading