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 };