From 5fbbc6a5bb0f34bf9cb539709cf61809689b1590 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 18:33:05 -0700 Subject: [PATCH 1/9] fix(types): String toLocale{Lower,Upper}Case + wider localeCompare arity Adds the two `toLocale{Lower,Upper}Case(locales?)` members to the string built-in surface (they were absent entirely -> spurious TS2339) and widens `localeCompare` from 1 to 3 params `(that, locales?, options?)` (a 2-arg call was tripping TS2554). Mirrors the two new names into StringMemberNames so the BuiltInApparentMembers invariant holds. Purely additive (RequiredParams unchanged) so it can only remove spurious errors. Flips the es2020 conformance test `localesObjectArgument.ts` from 6 spurious errors to clean. First fix from the #99 Track-1 spurious-error triage (17 conformance tests that tsc accepts but SharpTS wrongly flagged). Part of #80. xUnit 15425/0; conformance baseline 31/31 no drift. --- Runtime/BuiltIns/BuiltInTypes.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Runtime/BuiltIns/BuiltInTypes.cs b/Runtime/BuiltIns/BuiltInTypes.cs index cae94a23..de872a03 100644 --- a/Runtime/BuiltIns/BuiltInTypes.cs +++ b/Runtime/BuiltIns/BuiltInTypes.cs @@ -56,6 +56,10 @@ public static class BuiltInTypes "indexOf" => new TypeInfo.Function([StringType, NumberType], NumberType, RequiredParams: 1), "toUpperCase" => new TypeInfo.Function([], StringType), "toLowerCase" => new TypeInfo.Function([], StringType), + // toLocale{Lower,Upper}Case(locales?) — optional locale arg (string | string[] | Intl.Locale; + // Any covers every overload the runtime accepts). ECMA-402 §String.prototype. + "toLocaleLowerCase" => new TypeInfo.Function([AnyType], StringType, RequiredParams: 0), + "toLocaleUpperCase" => new TypeInfo.Function([AnyType], StringType, RequiredParams: 0), "trim" => new TypeInfo.Function([], StringType), // replace(searchValue, replaceValue) accepts string | RegExp for the // pattern and string | (match, ...groups) => string for the value. @@ -87,7 +91,9 @@ public static class BuiltInTypes "replaceAll" => new TypeInfo.Function([AnyType, AnyType], StringType), "at" => new TypeInfo.Function([NumberType], StringType), // returns string | undefined in TS "normalize" => new TypeInfo.Function([StringType], StringType, RequiredParams: 0), - "localeCompare" => new TypeInfo.Function([StringType], NumberType, RequiredParams: 0), + // localeCompare(that, locales?, options?) — ECMA-402 adds the optional + // locales/options args on top of ECMA-262's single `that` param. + "localeCompare" => new TypeInfo.Function([StringType, AnyType, AnyType], NumberType, RequiredParams: 0), "toString" => new TypeInfo.Function([], StringType), // primitive wrapper method "match" => new TypeInfo.Function([AnyType], AnyType), "matchAll" => new TypeInfo.Function([AnyType], new TypeInfo.Array(AnyType)), @@ -1308,7 +1314,8 @@ [new TypeInfo.Function([elementType, NumberType], BooleanType)], "length", "charAt", "substring", "indexOf", "toUpperCase", "toLowerCase", "trim", "replace", "split", "includes", "startsWith", "endsWith", "slice", "substr", "repeat", "padStart", "padEnd", "charCodeAt", "codePointAt", "concat", "lastIndexOf", "trimStart", "trimEnd", - "replaceAll", "at", "normalize", "localeCompare", "toString", "match", "matchAll", "search", + "replaceAll", "at", "normalize", "localeCompare", "toLocaleLowerCase", "toLocaleUpperCase", + "toString", "match", "matchAll", "search", ]; private static readonly string[] ArrayMemberNames = From bd0bb66256f3c080d83f10e5b1acb7c115856f62 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 18:34:09 -0700 Subject: [PATCH 2/9] test(conformance): expand subset to lib-sensitive folders (Symbols, es2016-2023) Adds the ES-version library folders (Symbols, es6/Symbols, es2016..es2023, esnext) to the conformance subset: 131 -> 296 tests. New committed buckets: 182 Pass / 75 Fail / 24 ParseError / 8 multi-file / 3 lib-drift / 3 directive / 1 TypeCheckError. This is the measurement-spike subset from the #99 pre-condition-2 work, now made permanent so Track-1 checker/parser fixes register as visible Fail->Pass flips and any regression hard-fails the baseline. The Fail and ParseError buckets are the known-state work-list, not passing tests. localesObjectArgument already lands Pass here (the just-committed String locale fix). Part of #80 / #99. --- .../baselines/interpreted.txt | 165 ++++++++++++++++++ .../config/subset.json | 13 +- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 744e7a3b..55d2d31d 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -1,4 +1,169 @@ # TS conformance baseline — do not hand-edit. Regenerate with SHARPTS_TSCONFORMANCE_UPDATE_BASELINE=1. +tests/cases/conformance/Symbols/ES5SymbolProperty1.ts Pass +tests/cases/conformance/Symbols/ES5SymbolProperty2.ts Fail +tests/cases/conformance/Symbols/ES5SymbolProperty3.ts Pass +tests/cases/conformance/Symbols/ES5SymbolProperty4.ts Pass +tests/cases/conformance/Symbols/ES5SymbolProperty5.ts Pass +tests/cases/conformance/Symbols/ES5SymbolProperty6.ts Fail +tests/cases/conformance/Symbols/ES5SymbolProperty7.ts Pass +tests/cases/conformance/Symbols/ES5SymbolType1.ts Pass +tests/cases/conformance/es2016/es2016IntlAPIs.ts Pass +tests/cases/conformance/es2017/es2017DateAPIs.ts Pass +tests/cases/conformance/es2017/useObjectValuesAndEntries1.ts Pass +tests/cases/conformance/es2017/useObjectValuesAndEntries2.ts Fail +tests/cases/conformance/es2017/useObjectValuesAndEntries3.ts Fail +tests/cases/conformance/es2017/useObjectValuesAndEntries4.ts Pass +tests/cases/conformance/es2017/useSharedArrayBuffer1.ts Pass +tests/cases/conformance/es2017/useSharedArrayBuffer2.ts Fail +tests/cases/conformance/es2017/useSharedArrayBuffer3.ts Fail +tests/cases/conformance/es2017/useSharedArrayBuffer4.ts Fail +tests/cases/conformance/es2017/useSharedArrayBuffer5.ts Fail +tests/cases/conformance/es2017/useSharedArrayBuffer6.ts Skipped:lib-drift +tests/cases/conformance/es2018/es2018IntlAPIs.ts Pass +tests/cases/conformance/es2018/invalidTaggedTemplateEscapeSequences.ts ParseError +tests/cases/conformance/es2018/usePromiseFinally.ts Pass +tests/cases/conformance/es2018/useRegexpGroups.ts Pass +tests/cases/conformance/es2019/allowUnescapedParagraphAndLineSeparatorsInStringLiteral.ts Pass +tests/cases/conformance/es2019/globalThisAmbientModules.ts Fail +tests/cases/conformance/es2019/globalThisBlockscopedProperties.ts Fail +tests/cases/conformance/es2019/globalThisCollision.ts Skipped:directive:allowjs +tests/cases/conformance/es2019/globalThisGlobalExportAsGlobal.ts ParseError +tests/cases/conformance/es2019/globalThisPropertyAssignment.ts Skipped:directive:allowjs +tests/cases/conformance/es2019/globalThisReadonlyProperties.ts Fail +tests/cases/conformance/es2019/globalThisTypeIndexAccess.ts Fail +tests/cases/conformance/es2019/globalThisUnknown.ts Fail +tests/cases/conformance/es2019/globalThisUnknownNoImplicitAny.ts Fail +tests/cases/conformance/es2019/globalThisVarDeclaration.ts Skipped:directive:allowjs +tests/cases/conformance/es2019/importMeta/importMeta.ts Skipped:multi-file-deferred +tests/cases/conformance/es2019/importMeta/importMetaNarrowing.ts ParseError +tests/cases/conformance/es2020/bigintMissingES2019.ts Fail +tests/cases/conformance/es2020/bigintMissingES2020.ts Fail +tests/cases/conformance/es2020/bigintMissingESNext.ts Fail +tests/cases/conformance/es2020/constructBigint.ts Fail +tests/cases/conformance/es2020/es2020IntlAPIs.ts Fail +tests/cases/conformance/es2020/intlNumberFormatES2020.ts Pass +tests/cases/conformance/es2020/localesObjectArgument.ts Pass +tests/cases/conformance/es2020/modules/exportAsNamespace1.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace2.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace3.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace4.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace5.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace_exportAssignment.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace_missingEmitHelpers.ts Skipped:multi-file-deferred +tests/cases/conformance/es2020/modules/exportAsNamespace_nonExistent.ts ParseError +tests/cases/conformance/es2021/es2021LocalesObjectArgument.ts Pass +tests/cases/conformance/es2021/intlDateTimeFormatRangeES2021.ts Pass +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment1.ts Pass +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment10.ts Pass +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment2.ts ParseError +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment3.ts ParseError +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment4.ts Fail +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment5.ts Pass +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment6.ts Fail +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment7.ts Fail +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment8.ts Fail +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment9.ts Pass +tests/cases/conformance/es2022/es2022IntlAPIs.ts Pass +tests/cases/conformance/es2022/es2022LocalesObjectArgument.ts Pass +tests/cases/conformance/es2022/es2022SharedMemory.ts ParseError +tests/cases/conformance/es2023/intlNumberFormatES2023.ts Pass +tests/cases/conformance/es2023/intlNumberFormatES5UseGrouping.ts Fail +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit1.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit10.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit11.ts ParseError +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit12.ts Fail +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit13.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit14.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit2.ts ParseError +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit3.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit4.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit5.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit6.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit7.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit8.ts Pass +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit9.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty1.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty10.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty11.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty12.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty13.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty14.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty15.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty16.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty17.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty18.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty19.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty2.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty20.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty21.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty22.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty23.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty24.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty25.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty26.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty27.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty28.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty29.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty3.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty30.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty33.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty36.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty37.ts TypeCheckError +tests/cases/conformance/es6/Symbols/symbolProperty38.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty39.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty4.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty40.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty41.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty42.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty43.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty44.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty45.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty46.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty47.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty48.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty49.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty5.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty50.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty51.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty52.ts Skipped:lib-drift +tests/cases/conformance/es6/Symbols/symbolProperty53.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty54.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty55.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty56.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty57.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty58.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty59.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty6.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty60.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty61.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty7.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty8.ts ParseError +tests/cases/conformance/es6/Symbols/symbolProperty9.ts ParseError +tests/cases/conformance/es6/Symbols/symbolType1.ts Fail +tests/cases/conformance/es6/Symbols/symbolType10.ts Fail +tests/cases/conformance/es6/Symbols/symbolType11.ts Pass +tests/cases/conformance/es6/Symbols/symbolType12.ts Fail +tests/cases/conformance/es6/Symbols/symbolType13.ts Fail +tests/cases/conformance/es6/Symbols/symbolType14.ts Fail +tests/cases/conformance/es6/Symbols/symbolType15.ts Fail +tests/cases/conformance/es6/Symbols/symbolType16.ts Pass +tests/cases/conformance/es6/Symbols/symbolType17.ts ParseError +tests/cases/conformance/es6/Symbols/symbolType18.ts ParseError +tests/cases/conformance/es6/Symbols/symbolType19.ts Fail +tests/cases/conformance/es6/Symbols/symbolType2.ts Fail +tests/cases/conformance/es6/Symbols/symbolType20.ts Fail +tests/cases/conformance/es6/Symbols/symbolType3.ts Fail +tests/cases/conformance/es6/Symbols/symbolType4.ts Fail +tests/cases/conformance/es6/Symbols/symbolType5.ts Fail +tests/cases/conformance/es6/Symbols/symbolType6.ts Fail +tests/cases/conformance/es6/Symbols/symbolType7.ts Fail +tests/cases/conformance/es6/Symbols/symbolType8.ts Fail +tests/cases/conformance/es6/Symbols/symbolType9.ts Fail +tests/cases/conformance/esnext/logicalAssignment/logicalAssignment11.ts Fail tests/cases/conformance/types/conditional/conditionalTypes1.ts Pass tests/cases/conformance/types/conditional/conditionalTypes2.ts Pass tests/cases/conformance/types/conditional/conditionalTypesExcessProperties.ts Pass diff --git a/SharpTS.TypeScriptConformance/config/subset.json b/SharpTS.TypeScriptConformance/config/subset.json index 584622a6..3f5fba7b 100644 --- a/SharpTS.TypeScriptConformance/config/subset.json +++ b/SharpTS.TypeScriptConformance/config/subset.json @@ -2,7 +2,18 @@ "folders": [ "tests/cases/conformance/types/typeRelationships/assignmentCompatibility", "tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes", - "tests/cases/conformance/types/conditional" + "tests/cases/conformance/types/conditional", + "tests/cases/conformance/Symbols", + "tests/cases/conformance/es6/Symbols", + "tests/cases/conformance/es2016", + "tests/cases/conformance/es2017", + "tests/cases/conformance/es2018", + "tests/cases/conformance/es2019", + "tests/cases/conformance/es2020", + "tests/cases/conformance/es2021", + "tests/cases/conformance/es2022", + "tests/cases/conformance/es2023", + "tests/cases/conformance/esnext" ], "timeoutSeconds": 5, "skipDirectivesFile": "skip-directives.txt", From f6b8c1cca635e91f095c83ef1a6427dba6e82c00 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 18:34:47 -0700 Subject: [PATCH 3/9] =?UTF-8?q?docs(status):=20refresh=20TS=20conformance?= =?UTF-8?q?=20section=20(=C2=A717)=20to=20296-test=20subset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table was stale (79 tests / 16 Pass, from the original 3-folder subset). Updates to the current committed buckets (296: 182 Pass / 75 Fail / 24 ParseError / 14 Skipped / 1 TypeCheckError) and folds in the #99 spike finding: lib.d.ts loading is an enabler, not the current pass-rate lever — checker breadth and parser gaps dominate the divergence. --- STATUS.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/STATUS.md b/STATUS.md index 69e8d277..baf46dd0 100644 --- a/STATUS.md +++ b/STATUS.md @@ -503,15 +503,20 @@ Two external corpora pin SharpTS against canonical references. Both run as stand `SharpTS.TypeScriptConformance/` runs a subset of [microsoft/TypeScript's conformance corpus](https://github.com/microsoft/TypeScript/tree/main/tests/cases/conformance) and diffs our type-checker diagnostics against `tsc`'s `*.errors.txt` baselines. Pinned to TS v5.5.4. Pass classification is on `(line, tsCode)` tuples — see [#80](https://github.com/nickna/SharpTS/issues/80) for the tracking epic. -| Subset | Tests | Pass | Fail | ParseError | Skipped | -|---|---:|---:|---:|---:|---:| -| `types/typeRelationships/assignmentCompatibility/` | 70 | 16 (22.9%) | 50 | 4 | 0 | -| `types/conditional/` | 9 | 0 | 5 | 3 | 1 | -| **Total** | **79** | **16 (20.3%)** | **55** | **7** | **1** | +The committed subset spans `types/typeRelationships/{assignmentCompatibility,subtypesAndSuperTypes}`, `types/conditional`, and the ES-version library folders (`Symbols`, `es6/Symbols`, `es2016`–`es2023`, `esnext`): -The parser is no longer the bottleneck. An extended parser sweep took the subset's `ParseError` count from 57 → 7 (≈86%): ambient `declare` of non-class declarations, `declare function`, generic/this/conditional/mapped/indexed-access/constructor/leading-operator types, `module Foo {}` namespaces, call/construct/index signatures, keyword & string/numeric property names, function-type and arrow optional/rest parameters, and more. Negative-test matching itself was unlocked by propagating canonical `TSnnnn` codes through the type-checker's error-recovery path ([#125](https://github.com/nickna/SharpTS/issues/125)), which also added a `strictNullChecks` option (default on; the runner follows each test's `@strict`/`@strictNullChecks` directive). +| Bucket | Count | Share | +|---|---:|---:| +| `Pass` | 182 | 61.5% | +| `Fail` | 75 | 25.3% | +| `ParseError` | 24 | 8.1% | +| `Skipped` (multi-file 8 / lib-drift 3 / directive 3) | 14 | 4.7% | +| `TypeCheckError` | 1 | 0.3% | +| **Total** | **296** | | -The dominant bucket is now `Fail` — tests that parse and reach the type checker but whose diagnostic set doesn't yet match `tsc`'s. These are mostly the `assignmentCompatibility` cases, which require deeper callable / constructable / structural assignability. The largest remaining levers are structural class-to-class assignability ([#129](https://github.com/nickna/SharpTS/issues/129) — SharpTS is nominal by design) and loading `tsc`'s `lib.*.d.ts` ([#99](https://github.com/nickna/SharpTS/issues/99)). +The parser is no longer the dominant bottleneck (an earlier sweep took the original subset's `ParseError` count from 57 → 7): ambient `declare` of non-class declarations, `declare function`, generic/this/conditional/mapped/indexed-access/constructor/leading-operator types, `module Foo {}` namespaces, call/construct/index signatures, keyword & string/numeric property names, function-type and arrow optional/rest parameters, and more. Negative-test matching itself was unlocked by propagating canonical `TSnnnn` codes through the type-checker's error-recovery path ([#125](https://github.com/nickna/SharpTS/issues/125)), which also added a `strictNullChecks` option (default on; the runner follows each test's `@strict`/`@strictNullChecks` directive). The residual `ParseError` bucket is now concentrated in the `symbolProperty*`/`symbolType*` tests — computed well-known-symbol names in *value/class* position (the [#99](https://github.com/nickna/SharpTS/issues/99) Phase-A parser work only hardened *type* position). + +The dominant bucket is `Fail` — tests that parse and reach the type checker but whose diagnostic set doesn't yet match `tsc`'s. A [#99](https://github.com/nickna/SharpTS/issues/99) measurement spike over the lib-sensitive folders decomposed these: of 66 lib-folder Fails, ~17 are **spurious** (SharpTS emits an error `tsc` doesn't), a long tail are individual checker gaps (assignability, symbol arithmetic, computed-name constraints), and only ~6 are explicit lib-version drift (`TS2550`/`TS2583`/`TS2585`). **The finding: loading `tsc`'s `lib.*.d.ts` is _not_ the current pass-rate lever — checker breadth and parser gaps dominate the divergence.** Loading `lib.*.d.ts` ([#99](https://github.com/nickna/SharpTS/issues/99)) remains valuable as an *enabler* (it removes the "`Pass` = coincidence" asterisk since globals resolve to `any` today, unblocks DOM/JSX wholesale, and is a prerequisite for per-node types/symbols in [#88](https://github.com/nickna/SharpTS/issues/88)), but the near-term levers are checker correctness (kill the spurious-error cases; structural class-to-class assignability [#129](https://github.com/nickna/SharpTS/issues/129)) and finishing symbol-name parsing. `Skipped` includes: - **Multi-file tests** (`Skipped:multi-file-deferred`) — cross-file resolution into the runner is follow-up work. From c0e0697cf14fd5cd3f36e3504fb8f0d9d01b1f56 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 18:44:21 -0700 Subject: [PATCH 4/9] fix(types): `object` is assignable to an object type with no required members The non-primitive `object` type was only relatable to object/any/unknown, so `object` was wrongly rejected against `{}` and all-optional shapes like `{ t?: string }` (spurious TS2344 on a generic constraint). tsc treats `object` as assignable to any target that requires nothing of it. Broadens the `object`-as-source arm in TryRelateNeverUnknownObject to also accept a Record whose fields are all optional and which has no call / construct / index signatures (new HasNoRequiredMembers helper). Second fix from the #99 Track-1 spurious-error triage. Flips the three es2020 conformance tests bigintMissingES2019/ES2020/ESNext Fail -> Pass (0 regressions, conformance Pass 182 -> 185). Part of #80. xUnit 15425/0. --- .../baselines/interpreted.txt | 6 ++--- .../TypeChecker.Compatibility.Relations.cs | 23 +++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 55d2d31d..2399673b 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -36,9 +36,9 @@ tests/cases/conformance/es2019/globalThisUnknownNoImplicitAny.ts Fail tests/cases/conformance/es2019/globalThisVarDeclaration.ts Skipped:directive:allowjs tests/cases/conformance/es2019/importMeta/importMeta.ts Skipped:multi-file-deferred tests/cases/conformance/es2019/importMeta/importMetaNarrowing.ts ParseError -tests/cases/conformance/es2020/bigintMissingES2019.ts Fail -tests/cases/conformance/es2020/bigintMissingES2020.ts Fail -tests/cases/conformance/es2020/bigintMissingESNext.ts Fail +tests/cases/conformance/es2020/bigintMissingES2019.ts Pass +tests/cases/conformance/es2020/bigintMissingES2020.ts Pass +tests/cases/conformance/es2020/bigintMissingESNext.ts Pass tests/cases/conformance/es2020/constructBigint.ts Fail tests/cases/conformance/es2020/es2020IntlAPIs.ts Fail tests/cases/conformance/es2020/intlNumberFormatES2020.ts Pass diff --git a/TypeSystem/TypeChecker.Compatibility.Relations.cs b/TypeSystem/TypeChecker.Compatibility.Relations.cs index 021a6fce..f2975d08 100644 --- a/TypeSystem/TypeChecker.Compatibility.Relations.cs +++ b/TypeSystem/TypeChecker.Compatibility.Relations.cs @@ -136,16 +136,35 @@ private bool TryRelateNeverUnknownObject(TypeInfo expected, TypeInfo actual, out return true; } - // object as actual: can only assign to object, any, unknown + // object as actual: assignable to object/any/unknown, and to any object + // type that demands nothing of it — `{}` or an all-optional shape like + // `{ t?: string }`. The bare non-primitive `object` satisfies such a + // target because it has no required members to be missing (matches tsc). if (actual is TypeInfo.Object) { - result = expected is TypeInfo.Object or TypeInfo.Any or TypeInfo.Unknown; + result = expected is TypeInfo.Object or TypeInfo.Any or TypeInfo.Unknown + || (expected is TypeInfo.Record rec && HasNoRequiredMembers(rec)); return true; } result = false; return false; } + /// + /// True when an object type requires nothing of its source: every declared + /// field is optional and there are no call / construct / index signatures. + /// The bare non-primitive object type is assignable to such a target + /// ({}, { t?: string }), matching tsc. + /// + private static bool HasNoRequiredMembers(TypeInfo.Record rec) + { + if (rec.HasCallSignature || rec.HasConstructorSignature || rec.HasIndexSignature) + return false; + foreach (var name in rec.Fields.Keys) + if (!rec.IsFieldOptional(name)) return false; + return true; + } + /// /// Null / undefined as source under strictNullChecks (the non-strict case is /// handled at the very top of IsCompatibleCore): assignable only to a matching From 445bdda28df4720b3b51f0c643390c2365578ba1 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 19:07:50 -0700 Subject: [PATCH 5/9] test(conformance): resolve per-target `name(target=X).errors.txt` baselines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-target tests (`// @target: es2015, es2020, ...`) emit one baseline per target and NO plain `name.errors.txt`. ResolveBaselinePath only looked for the plain file, so these tests were scored against an empty baseline — any real diagnostic mis-counted as spurious, and (worse) a test where SharpTS produces nothing but tsc expects errors scored as a FALSE PASS. Adds newest-target resolution (es3 < es5 < es2015 < ... < esnext), matching SharpTS's single always-latest world model. 84 corpus tests / 6 in the current subset were affected. Exposes one hidden false pass: logicalAssignment5 (Pass -> Fail) — tsc reports 4 possibly-undefined errors on its `&&=` cases that SharpTS misses entirely. Honest Fail now; a real target for the deferred Cluster-A (logical-assignment narrowing) work. Part of #80 / #99. --- .../TypeScriptConformanceRunner.cs | 55 ++++++++++++++++++- .../baselines/interpreted.txt | 2 +- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/SharpTS.TypeScriptConformance/TypeScriptConformanceRunner.cs b/SharpTS.TypeScriptConformance/TypeScriptConformanceRunner.cs index c3572ae6..ee59efb2 100644 --- a/SharpTS.TypeScriptConformance/TypeScriptConformanceRunner.cs +++ b/SharpTS.TypeScriptConformance/TypeScriptConformanceRunner.cs @@ -203,15 +203,64 @@ public TypeScriptConformanceResult RunOne(string testFilePath) /// tests/baselines/reference/<testname>.errors.txt (flat directory, /// no folder mirroring). Returns the expected path even if the file /// doesn't exist — caller treats absence as "no expected diagnostics." + /// + /// Multi-target tests (// @target: es2015, es2020, ...) emit one + /// baseline per target — name(target=X).errors.txt — and no plain + /// file. SharpTS has a single always-latest world model (globals are always + /// available, no per-target lib), so we compare against the newest available + /// target variant. Without this, those tests are scored against an empty + /// baseline and every real diagnostic is mis-counted as spurious. /// private string ResolveBaselinePath(string testFilePath) { var basename = Path.GetFileNameWithoutExtension(testFilePath); - return Path.Combine( - TypeScriptConformancePaths.BaselinesDir(_typescriptRoot), - $"{basename}.errors.txt"); + var dir = TypeScriptConformancePaths.BaselinesDir(_typescriptRoot); + var plain = Path.Combine(dir, $"{basename}.errors.txt"); + if (File.Exists(plain)) return plain; + return ResolveNewestTargetBaseline(dir, basename) ?? plain; } + /// + /// Newest-target name(target=X).errors.txt baseline for a multi-target + /// test, or null if none exist. "Newest" follows ES ordering + /// (es3 < es5 < es2015 < ... < esnext) so the chosen baseline + /// matches SharpTS's always-latest lib surface. + /// + private static string? ResolveNewestTargetBaseline(string baselinesDir, string basename) + { + string? best = null; + var bestRank = int.MinValue; + foreach (var path in Directory.EnumerateFiles(baselinesDir, $"{basename}(target=*).errors.txt")) + { + var file = Path.GetFileName(path); + const string marker = "(target="; + var open = file.IndexOf(marker, StringComparison.Ordinal); + var close = file.IndexOf(").errors.txt", StringComparison.Ordinal); + if (open < 0 || close <= open + marker.Length) continue; + var target = file.Substring(open + marker.Length, close - (open + marker.Length)); + var rank = TargetRank(target); + if (rank > bestRank) { bestRank = rank; best = path; } + } + return best; + } + + private static int TargetRank(string target) => target.Trim().ToLowerInvariant() switch + { + "es3" => 3, + "es5" => 5, + "es6" or "es2015" => 2015, + "es2016" => 2016, + "es2017" => 2017, + "es2018" => 2018, + "es2019" => 2019, + "es2020" => 2020, + "es2021" => 2021, + "es2022" => 2022, + "es2023" => 2023, + "esnext" => int.MaxValue, + _ => 0, + }; + /// /// Converts SharpTS diagnostics into the (line, tsCode) match-key form. /// Drops diagnostics with no TsCode (SharpTS-only — see #95) — they diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 2399673b..8f520965 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -58,7 +58,7 @@ tests/cases/conformance/es2021/logicalAssignment/logicalAssignment10.ts Pass tests/cases/conformance/es2021/logicalAssignment/logicalAssignment2.ts ParseError tests/cases/conformance/es2021/logicalAssignment/logicalAssignment3.ts ParseError tests/cases/conformance/es2021/logicalAssignment/logicalAssignment4.ts Fail -tests/cases/conformance/es2021/logicalAssignment/logicalAssignment5.ts Pass +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment5.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment6.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment7.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment8.ts Fail From 967f12ee1d0352bfc9350f171d88321aaab9afd1 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 19:15:55 -0700 Subject: [PATCH 6/9] fix(types): resolve `typeof globalThis` in type position + `any` member typeof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The typeof-in-type-position path (EvaluateTypeOf) resolves names via the TypeEnvironment, which doesn't carry the hard-coded ambient globals that expression-position LookupVariable knows — so `(typeof globalThis)[...]` threw a spurious TS2304. Resolves `globalThis` to `any` there (SharpTS's global model), and makes property access on `any` in a typeof path stay `any` (GetPropertyTypeForTypeOf) so the trailing index doesn't then TS7053. Third fix from the #99 Track-1 spurious-error triage. Flips globalThisTypeIndexAccess Fail -> Pass; three more globalThis tests move Fail -> Skipped:lib-drift (they were false-Fails from the spurious TS2304 — now honestly deferred as lib-drift). 0 regressions. Part of #80. xUnit 15425/0. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 8 ++++---- TypeSystem/TypeChecker.TypeParsing.cs | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 8f520965..fc1bec6f 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -24,14 +24,14 @@ tests/cases/conformance/es2018/invalidTaggedTemplateEscapeSequences.ts ParseErro tests/cases/conformance/es2018/usePromiseFinally.ts Pass tests/cases/conformance/es2018/useRegexpGroups.ts Pass tests/cases/conformance/es2019/allowUnescapedParagraphAndLineSeparatorsInStringLiteral.ts Pass -tests/cases/conformance/es2019/globalThisAmbientModules.ts Fail -tests/cases/conformance/es2019/globalThisBlockscopedProperties.ts Fail +tests/cases/conformance/es2019/globalThisAmbientModules.ts Skipped:lib-drift +tests/cases/conformance/es2019/globalThisBlockscopedProperties.ts Skipped:lib-drift tests/cases/conformance/es2019/globalThisCollision.ts Skipped:directive:allowjs tests/cases/conformance/es2019/globalThisGlobalExportAsGlobal.ts ParseError tests/cases/conformance/es2019/globalThisPropertyAssignment.ts Skipped:directive:allowjs tests/cases/conformance/es2019/globalThisReadonlyProperties.ts Fail -tests/cases/conformance/es2019/globalThisTypeIndexAccess.ts Fail -tests/cases/conformance/es2019/globalThisUnknown.ts Fail +tests/cases/conformance/es2019/globalThisTypeIndexAccess.ts Pass +tests/cases/conformance/es2019/globalThisUnknown.ts Skipped:lib-drift tests/cases/conformance/es2019/globalThisUnknownNoImplicitAny.ts Fail tests/cases/conformance/es2019/globalThisVarDeclaration.ts Skipped:directive:allowjs tests/cases/conformance/es2019/importMeta/importMeta.ts Skipped:multi-file-deferred diff --git a/TypeSystem/TypeChecker.TypeParsing.cs b/TypeSystem/TypeChecker.TypeParsing.cs index 9716e217..09b176b5 100644 --- a/TypeSystem/TypeChecker.TypeParsing.cs +++ b/TypeSystem/TypeChecker.TypeParsing.cs @@ -522,6 +522,13 @@ private TypeInfo EvaluateTypeOf(string path) return new TypeInfo.Undefined(); TypeInfo? currentType = _environment.Get(firstName); + // `typeof globalThis` — the global object has no environment binding. + // SharpTS models globals as `any`, so it (and any member access off it) + // resolves to `any`. Other bare globals in typeof-type position remain a + // gap until lib.d.ts globals are loaded (#99). + if (currentType == null && firstName == "globalThis") + currentType = new TypeInfo.Any(); + if (currentType == null) throw new TypeCheckException($"Cannot find name '{firstName}' for typeof.", tsCode: "TS2304"); @@ -669,6 +676,8 @@ TypeInfo t when inst.ClassType is TypeInfo.Class c && TypeInfo.GenericClass gc => gc.StaticMethods.GetValueOrDefault(propName) ?? gc.StaticProperties.GetValueOrDefault(propName), TypeInfo.Namespace ns => ns.GetMember(propName), + // Property access on `any` stays `any` (e.g. `(typeof globalThis)["x"]`). + TypeInfo.Any => new TypeInfo.Any(), _ => null }; From 600e20255817b228345669e1d35127771a9d5d39 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 00:57:01 -0700 Subject: [PATCH 7/9] fix(types): narrow the value type of compound logical assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckLogicalAssign returned the un-narrowed declared type for every operator, so `(results ||= []).push(100)` and `(results ??= []).push(100)` — both valid TypeScript — were wrongly rejected as possibly-undefined. The value of `a OP= b` mirrors the binary `a OP b`: ||= -> Truthy(a) | b ??= -> NonNullish(a) | b &&= -> Falsy(a) | b Applies that narrowing (new NarrowLogicalTruthy / NarrowLogicalFalsy over union constituents; ExpandNonNullable for `??=`), so `||=`/`??=` drop undefined while `&&=` keeps it (still correctly flagged). Removes a real false-positive; foundation for the #99 Track-1 Cluster-A (logical-assignment) work. Does NOT yet flip the 5 logicalAssignment conformance tests — those additionally need cross-statement CFA narrowing (`x ??= []; x.push()`), `if (a op= b)` condition narrowing, and diagnostic codes aligned to tsc (TS2532/TS18048 vs our TS2533/TS2339) with never[] element handling (TS2345). Tracked as the residual Cluster-A effort. xUnit 15425/0; conformance baseline unchanged (no drift). Part of #80. --- TypeSystem/TypeChecker.Operators.cs | 68 +++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/TypeSystem/TypeChecker.Operators.cs b/TypeSystem/TypeChecker.Operators.cs index 4ea17aea..916e3066 100644 --- a/TypeSystem/TypeChecker.Operators.cs +++ b/TypeSystem/TypeChecker.Operators.cs @@ -431,11 +431,73 @@ private TypeInfo CheckLogicalAssign(Expr.LogicalAssign logical) var assignedPath = new Narrowing.NarrowingPath.Variable(logical.Name.Lexeme); InvalidateNarrowingsFor(assignedPath); - // Logical assignment can return either the current value or the new value - // For simplicity, return union of both types or the variable type - return varType; + // The value of `a OP= b` is the value of the binary `a OP b`: + // a ||= b -> Truthy(a) | b (a used when falsy -> replaced by b) + // a ??= b -> NonNullish(a) | b (a used when null/undefined -> b) + // a &&= b -> Falsy(a) | b (a kept when falsy, else b) + // Narrowing the left operand here is what makes `(results ||= []).push()` + // type-check (undefined dropped) while keeping `(results &&= []).push()` + // flagged (undefined kept). Without it every form kept the full declared + // type and `||=`/`??=` produced spurious possibly-undefined errors. + TypeInfo narrowedVar = logical.Operator.Type switch + { + TokenType.OR_OR_EQUAL => NarrowLogicalTruthy(varType), + TokenType.QUESTION_QUESTION_EQUAL => ExpandNonNullable(varType), + TokenType.AND_AND_EQUAL => NarrowLogicalFalsy(varType), + _ => varType, + }; + + if (narrowedVar is TypeInfo.Never) return valueType; + if (narrowedVar is TypeInfo.Any || valueType is TypeInfo.Any) return new TypeInfo.Any(); + if (IsCompatible(narrowedVar, valueType)) return valueType; + if (IsCompatible(valueType, narrowedVar)) return narrowedVar; + return new TypeInfo.Union([narrowedVar, valueType]); + } + + /// Truthy narrowing of a type: drops the definitely-falsy constituents + /// (null, undefined, and the literals false/0/""). Applied to + /// the left operand of || / ||=. + private static TypeInfo NarrowLogicalTruthy(TypeInfo type) + => FilterUnionConstituents(type, t => !IsDefinitelyFalsy(t)); + + /// Falsy narrowing of a type: keeps only the possibly-falsy constituents. + /// A type with none (e.g. a bare array or function) narrows to never. + /// Applied to the left operand of && / &&=. + private static TypeInfo NarrowLogicalFalsy(TypeInfo type) + => FilterUnionConstituents(type, t => !IsDefinitelyTruthy(t)); + + private static TypeInfo FilterUnionConstituents(TypeInfo type, Func keep) + { + if (type is TypeInfo.Union u) + { + var kept = u.FlattenedTypes.Where(keep).ToList(); + return kept.Count switch + { + 0 => new TypeInfo.Never(), + 1 => kept[0], + _ => new TypeInfo.Union(kept), + }; + } + return keep(type) ? type : new TypeInfo.Never(); } + /// A constituent that is always falsy: null, undefined, or a falsy literal. + private static bool IsDefinitelyFalsy(TypeInfo t) => t switch + { + TypeInfo.Null or TypeInfo.Undefined => true, + TypeInfo.BooleanLiteral bl => !bl.Value, + TypeInfo.NumberLiteral nl => nl.Value == 0, + TypeInfo.StringLiteral sl => sl.Value.Length == 0, + _ => false, + }; + + /// A constituent that is always truthy: object-like types (array, tuple, + /// class instance, record, interface, function, class, the non-primitive + /// object). Primitives and literals may be falsy and so are excluded. + private static bool IsDefinitelyTruthy(TypeInfo t) => t is + TypeInfo.Array or TypeInfo.Tuple or TypeInfo.Instance or TypeInfo.Record + or TypeInfo.Interface or TypeInfo.Function or TypeInfo.Class or TypeInfo.Object; + private TypeInfo CheckLogicalSet(Expr.LogicalSet logical) { CheckExpr(logical.Object); From 1cd8b6be43313e754a39a458cd9b3a47ee16b50b Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 01:59:45 -0700 Subject: [PATCH 8/9] fix(types): split nullable member-access diagnostic to match tsc codes Non-optional property access on a nullable union always threw TS2533 ("possibly null or undefined"), but tsc picks the code from which of null/undefined the union actually carries and whether the receiver is a bare identifier: expression receiver: TS2531 (null) / TS2532 (undefined) / TS2533 (both) identifier receiver: TS18047 / TS18048 / TS18049 Collapsing all six into TS2533 mismatched most strict-null baselines. CheckGetOnUnion now threads the receiver expression and emits the matching code (new NullableMemberAccessError). The message keeps the property name (more actionable than tsc's bare form; conformance matches on code only). Fourth #99 Track-1 fix. Flips logicalAssignment8 Fail -> Pass (its `(results &&= ...).push()` receiver is undefined-only -> TS2532); the split also aligns nullable-access codes corpus-wide. 0 regressions. xUnit 15425/0; conformance Pass 185 -> 186. Part of #80. --- .../baselines/interpreted.txt | 2 +- TypeSystem/TypeChecker.Properties.cs | 39 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index fc1bec6f..ba249fe4 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -61,7 +61,7 @@ tests/cases/conformance/es2021/logicalAssignment/logicalAssignment4.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment5.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment6.ts Fail tests/cases/conformance/es2021/logicalAssignment/logicalAssignment7.ts Fail -tests/cases/conformance/es2021/logicalAssignment/logicalAssignment8.ts Fail +tests/cases/conformance/es2021/logicalAssignment/logicalAssignment8.ts Pass tests/cases/conformance/es2021/logicalAssignment/logicalAssignment9.ts Pass tests/cases/conformance/es2022/es2022IntlAPIs.ts Pass tests/cases/conformance/es2022/es2022LocalesObjectArgument.ts Pass diff --git a/TypeSystem/TypeChecker.Properties.cs b/TypeSystem/TypeChecker.Properties.cs index b1fd5ad4..45d0a6b7 100644 --- a/TypeSystem/TypeChecker.Properties.cs +++ b/TypeSystem/TypeChecker.Properties.cs @@ -195,7 +195,7 @@ private TypeInfo ResolveMemberType(Expr.Get get, TypeInfo objType) TypeCategory.Namespace when objType is TypeInfo.Namespace nsType => CheckGetOnNamespace(nsType, get.Name), TypeCategory.Union when objType is TypeInfo.Union union => - CheckGetOnUnion(union, get.Name, get.Optional), + CheckGetOnUnion(union, get.Name, get.Optional, get.Object), TypeCategory.Intersection when objType is TypeInfo.Intersection intersection => CheckGetOnIntersection(intersection, get.Name), _ => new TypeInfo.Any() @@ -209,7 +209,7 @@ private TypeInfo ResolveMemberType(Expr.Get get, TypeInfo objType) /// If a property doesn't exist on some non-null/undefined members, the result /// includes undefined for those members (mimicking TypeScript's permissive behavior). /// - private TypeInfo CheckGetOnUnion(TypeInfo.Union union, Token memberName, bool isOptional = false) + private TypeInfo CheckGetOnUnion(TypeInfo.Union union, Token memberName, bool isOptional = false, Expr? receiver = null) { List memberTypes = []; bool hasNullOrUndefined = false; @@ -226,7 +226,7 @@ private TypeInfo CheckGetOnUnion(TypeInfo.Union union, Token memberName, bool is hasNullOrUndefined = true; continue; } - throw new TypeCheckException($"Property '{memberName.Lexeme}' cannot be accessed on '{member}'. Object is possibly null or undefined.", memberName.Line, tsCode: "TS2533"); + throw NullableMemberAccessError(union, memberName, receiver); } try @@ -265,6 +265,39 @@ private TypeInfo CheckGetOnUnion(TypeInfo.Union union, Token memberName, bool is return unique.Count == 1 ? unique[0] : new TypeInfo.Union(unique); } + /// + /// Builds the "possibly null/undefined" error for non-optional member access on + /// a nullable union, picking the same diagnostic tsc would. The code depends on + /// which of null/undefined the union carries and on whether the receiver is a bare + /// identifier: identifiers use TS18047/18048/18049 ("'x' is possibly ..."), other + /// expressions use TS2531/2532/2533 ("Object is possibly ..."). Collapsing all six + /// into TS2533 (the old behaviour) mismatched most strict-null conformance baselines. + /// + private static TypeCheckException NullableMemberAccessError(TypeInfo.Union union, Token memberName, Expr? receiver) + { + bool hasNull = union.ContainsNull; + bool hasUndefined = union.ContainsUndefined; + string? ident = receiver is Expr.Variable v ? v.Name.Lexeme : null; + + // The subject clause matches tsc's wording; the code is picked to match too. + // (SharpTS keeps the member name in the message — more actionable than tsc's + // bare form — since conformance matching is on the code, not the text.) + (string code, string subject) = (hasNull, hasUndefined, ident) switch + { + (true, false, null) => ("TS2531", "Object is possibly 'null'."), + (false, true, null) => ("TS2532", "Object is possibly 'undefined'."), + (true, true, null) => ("TS2533", "Object is possibly 'null' or 'undefined'."), + (true, false, { } n) => ("TS18047", $"'{n}' is possibly 'null'."), + (false, true, { } n) => ("TS18048", $"'{n}' is possibly 'undefined'."), + (true, true, { } n) => ("TS18049", $"'{n}' is possibly 'null' or 'undefined'."), + // Reached only if the forcing member was null/undefined but the flags say + // otherwise (shouldn't happen); keep the broadest code as a safe default. + _ => ("TS2533", "Object is possibly 'null' or 'undefined'."), + }; + return new TypeCheckException( + $"Property '{memberName.Lexeme}' cannot be accessed. {subject}", memberName.Line, tsCode: code); + } + /// /// Type checks property access on an intersection type. /// The property is looked up on each member type until found. From a38dc2d6a91a72b42b175669564eb286fe27fa27 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 02:19:13 -0700 Subject: [PATCH 9/9] fix(types): allow well-known-symbol indexing on built-in reference types A symbol index (`x[Symbol.species]`, `x[Symbol.toStringTag]`, ...) resolved to `any` only for Record/Interface/Instance/RegExp receivers; the built-in reference types (SharedArrayBuffer, ArrayBuffer, typed arrays, Map, Promise, Date, ...) fell through to a spurious TS7053. Well-known symbols are present on essentially every object, and SharpTS models symbol members loosely, so this widens the existing "symbol index -> any" arm via a shared IsSymbolIndexableObject predicate. #99 Track-1 Cluster B (well-known-symbol indexing). Flips es2017 useSharedArrayBuffer4 & useSharedArrayBuffer5 Fail -> Pass. 0 regressions. (symbolProperty18 still needs per-well-known-symbol member modeling on the WRITE index path; useSharedArrayBuffer2/3 are lib-drift, need #99 lib.d.ts.) xUnit 15425/0; conformance Pass 186 -> 188. Part of #80. --- .../baselines/interpreted.txt | 4 +-- TypeSystem/TypeChecker.Properties.Index.cs | 28 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index ba249fe4..c72c82f8 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -16,8 +16,8 @@ tests/cases/conformance/es2017/useObjectValuesAndEntries4.ts Pass tests/cases/conformance/es2017/useSharedArrayBuffer1.ts Pass tests/cases/conformance/es2017/useSharedArrayBuffer2.ts Fail tests/cases/conformance/es2017/useSharedArrayBuffer3.ts Fail -tests/cases/conformance/es2017/useSharedArrayBuffer4.ts Fail -tests/cases/conformance/es2017/useSharedArrayBuffer5.ts Fail +tests/cases/conformance/es2017/useSharedArrayBuffer4.ts Pass +tests/cases/conformance/es2017/useSharedArrayBuffer5.ts Pass tests/cases/conformance/es2017/useSharedArrayBuffer6.ts Skipped:lib-drift tests/cases/conformance/es2018/es2018IntlAPIs.ts Pass tests/cases/conformance/es2018/invalidTaggedTemplateEscapeSequences.ts ParseError diff --git a/TypeSystem/TypeChecker.Properties.Index.cs b/TypeSystem/TypeChecker.Properties.Index.cs index 969570d3..e0b426ac 100644 --- a/TypeSystem/TypeChecker.Properties.Index.cs +++ b/TypeSystem/TypeChecker.Properties.Index.cs @@ -317,19 +317,33 @@ private TypeInfo CheckGetIndex(Expr.GetIndex getIndex) if (GetClassIndexType(objType, TokenType.TYPE_SYMBOL) is { } clsSym) return clsSym; - // Allow symbol bracket access on any object (returns any) - if (objType is TypeInfo.Record or TypeInfo.Interface or TypeInfo.Instance) - return new TypeInfo.Any(); - - // ECMA-262 §22.2.5: RegExp.prototype[@@match/@@matchAll/@@replace/@@search/@@split]. - // Treat as `any` to permit `r[Symbol.match](str)` etc. Runtime does the actual dispatch. - if (objType is TypeInfo.RegExp) + // Well-known symbols (Symbol.iterator/species/toStringTag/match/...) are + // present on essentially every object type. SharpTS models them loosely, + // so a symbol index on any object-like value resolves to `any` — this + // already covered Record/Interface/Instance; extend it to the built-in + // reference types (SharedArrayBuffer[Symbol.species], typed arrays, Map, + // Promise, ...) which otherwise fell through to a spurious TS7053. + if (IsSymbolIndexableObject(objType)) return new TypeInfo.Any(); } throw new TypeCheckException($" Index type '{indexType}' is not valid for indexing '{objType}'.", tsCode: "TS7053"); } + /// + /// Object-like types on which a well-known-symbol index (x[Symbol.iterator], + /// x[Symbol.species], ...) is permitted and resolves to any. Covers the + /// structural object types plus the built-in reference types; excludes primitives, + /// null/undefined, and unmodeled types (which keep their TS7053). + /// + private static bool IsSymbolIndexableObject(TypeInfo t) => t is + TypeInfo.Record or TypeInfo.Interface or TypeInfo.Instance or TypeInfo.Object or + TypeInfo.Array or TypeInfo.Tuple or TypeInfo.Function or TypeInfo.RegExp or + TypeInfo.Map or TypeInfo.Set or TypeInfo.WeakMap or TypeInfo.WeakSet or + TypeInfo.Promise or TypeInfo.Date or TypeInfo.Error or TypeInfo.Buffer or + TypeInfo.SharedArrayBuffer or TypeInfo.ArrayBuffer or TypeInfo.DataView or + TypeInfo.TypedArray; + /// /// Returns the value type of a class's index signature for the given key type, or null if the /// type is not a class instance (or has no such index signature). Accepts both a bare