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 =
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.
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 744e7a3b..c72c82f8 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 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
+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 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 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
+tests/cases/conformance/es2019/importMeta/importMetaNarrowing.ts ParseError
+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
+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 Fail
+tests/cases/conformance/es2021/logicalAssignment/logicalAssignment6.ts Fail
+tests/cases/conformance/es2021/logicalAssignment/logicalAssignment7.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
+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",
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
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);
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
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.
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
};