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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions SharpTS.TypeScriptConformance/baselines/interpreted.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ tests/cases/conformance/es6/Symbols/symbolProperty18.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty19.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty2.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty20.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty21.ts Fail
tests/cases/conformance/es6/Symbols/symbolProperty21.ts Pass
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/symbolProperty24.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty25.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty26.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty27.ts Pass
Expand Down Expand Up @@ -132,7 +132,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty51.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty52.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty53.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty54.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty55.ts Fail
tests/cases/conformance/es6/Symbols/symbolProperty55.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty56.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty57.ts Pass
tests/cases/conformance/es6/Symbols/symbolProperty58.ts Pass
Expand All @@ -149,7 +149,7 @@ tests/cases/conformance/es6/Symbols/symbolType11.ts Pass
tests/cases/conformance/es6/Symbols/symbolType12.ts Pass
tests/cases/conformance/es6/Symbols/symbolType13.ts Pass
tests/cases/conformance/es6/Symbols/symbolType14.ts Pass
tests/cases/conformance/es6/Symbols/symbolType15.ts Fail
tests/cases/conformance/es6/Symbols/symbolType15.ts Pass
tests/cases/conformance/es6/Symbols/symbolType16.ts Pass
tests/cases/conformance/es6/Symbols/symbolType17.ts Pass
tests/cases/conformance/es6/Symbols/symbolType18.ts Pass
Expand All @@ -162,7 +162,7 @@ tests/cases/conformance/es6/Symbols/symbolType5.ts Pass
tests/cases/conformance/es6/Symbols/symbolType6.ts Pass
tests/cases/conformance/es6/Symbols/symbolType7.ts Pass
tests/cases/conformance/es6/Symbols/symbolType8.ts Pass
tests/cases/conformance/es6/Symbols/symbolType9.ts Fail
tests/cases/conformance/es6/Symbols/symbolType9.ts Pass
tests/cases/conformance/esnext/logicalAssignment/logicalAssignment11.ts Pass
tests/cases/conformance/types/conditional/conditionalTypes1.ts Pass
tests/cases/conformance/types/conditional/conditionalTypes2.ts Pass
Expand Down Expand Up @@ -245,7 +245,7 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/undefine
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/unionTypesAssignability.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/enumIsNotASubtypeOfAnythingButNumber.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/nullIsSubtypeOfEverythingButUndefined.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/stringLiteralTypeIsSubtypeOfString.ts Fail
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/stringLiteralTypeIsSubtypeOfString.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfAny.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameter.ts Pass
tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints.ts Pass
Expand Down
6 changes: 6 additions & 0 deletions SharpTS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
<None Remove="stdlib\**\*.ts" />
</ItemGroup>

<!-- Ambient lib type declarations parsed by LibTypeLoader into the type checker (#99). -->
<ItemGroup>
<EmbeddedResource Include="libdefs\**\*.d.ts" />
<None Remove="libdefs\**\*.d.ts" />
</ItemGroup>

<!-- Exclude test, example, benchmark, and SDK projects from this compilation.
This list is hand-maintained and load-bearing: because this project sits at
the repo root, the SDK's default **/*.cs glob would otherwise pull in every
Expand Down
49 changes: 49 additions & 0 deletions TypeSystem/LibTypeLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Frozen;
using SharpTS.Parsing;

namespace SharpTS.TypeSystem;

/// <summary>
/// #99 increment 2 — the lib.d.ts loader. Parses the ambient TypeScript lib declarations embedded as
/// <c>libdefs/*.d.ts</c> resources and resolves them to modeled <see cref="TypeInfo"/>, so the type
/// checker resolves lib names (e.g. <c>SymbolConstructor</c>) from real declarations instead of
/// hand-modeled shapes. Built once and cached process-wide (the lib types are the same for every
/// checker instance). A later increment expands the embedded sources toward the full vendored
/// <c>external/typescript/src/lib/*.d.ts</c>.
/// </summary>
internal static class LibTypeLoader
{
private static FrozenDictionary<string, TypeInfo>? _types;

/// <summary>Ambient lib type name → its resolved <see cref="TypeInfo"/>.</summary>
public static FrozenDictionary<string, TypeInfo> Types => _types ??= Build();

public static bool TryGet(string name, out TypeInfo type)
{
if (Types.TryGetValue(name, out var t)) { type = t; return true; }
type = null!;
return false;
}

private static FrozenDictionary<string, TypeInfo> Build()
{
var statements = new List<Stmt>();
var assembly = typeof(LibTypeLoader).Assembly;
foreach (var resourceName in assembly.GetManifestResourceNames())
{
if (!resourceName.Contains("libdefs") || !resourceName.EndsWith(".d.ts"))
continue;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null) continue;
using var reader = new StreamReader(stream);
var parseResult = new Parser(new Lexer(reader.ReadToEnd()).ScanTokens()).Parse();
if (parseResult.IsSuccess)
statements.AddRange(parseResult.Statements);
}
// Resolve to TypeInfo via a throwaway checker; its diagnostics are discarded (lib sources are
// trusted). Empty-dict on any failure so a broken embed degrades to the prior hand-modeling.
return statements.Count > 0
? new TypeChecker().ExtractLibTypes(statements)
: FrozenDictionary<string, TypeInfo>.Empty;
}
}
14 changes: 14 additions & 0 deletions TypeSystem/TypeChecker.Calls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ private TypeInfo CheckCall(Expr.Call call)
var instantiatedFunc = InstantiateGenericFunction(genericFunc, typeArgs);
if (instantiatedFunc is TypeInfo.Function instFunc)
{
// Excess-property check for fresh object-literal args against the instantiated
// parameter types (the generic path otherwise skips argument validation).
for (int ai = 0; ai < call.Arguments.Count && ai < instFunc.ParamTypes.Count; ai++)
{
if (call.Arguments[ai] is Expr.ObjectLiteral && argTypes[ai] is TypeInfo.Record argRec)
CheckExcessProperties(argRec, instFunc.ParamTypes[ai], call.Arguments[ai]);
}
return instFunc.ReturnType;
}
return new TypeInfo.Any();
Expand Down Expand Up @@ -211,6 +218,13 @@ private TypeInfo CheckCall(Expr.Call call)
{
argType = CheckExpr(arg);
}
// Excess-property check for a FRESH object-literal argument (tsc's fresh-literal
// rule) against its parameter type — mirrors the assignment path.
if (arg is Expr.ObjectLiteral && argType is TypeInfo.Record argExcessRecord
&& paramIndex < regularParamCount)
{
CheckExcessProperties(argExcessRecord, funcType.ParamTypes[paramIndex], arg);
}
if (paramIndex < regularParamCount)
{
// Check against regular parameter. An optional/default-valued parameter
Expand Down
30 changes: 30 additions & 0 deletions TypeSystem/TypeChecker.Compatibility.Structural.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ private void CheckExcessProperties(TypeInfo.Record actual, TypeInfo expected, Ex
hasStringIndex = cls.StringIndexType != null;
hasNumberIndex = cls.NumberIndexType != null;
}
else if (expected is TypeInfo.InstantiatedGeneric ig && FlattenInstantiatedInterface(ig) is { } flatIface)
{
// A generic interface argument (`I<number>`) flattens to its concrete members so a fresh
// object literal passed to a generic function is excess-checked too.
foreach (var member in flatIface.GetAllMembers())
expectedKeys.Add(member.Key);
hasStringIndex = flatIface.StringIndexType != null;
hasNumberIndex = flatIface.NumberIndexType != null;
}
else
{
// For other types (primitives, unions, etc.), skip excess property check
Expand Down Expand Up @@ -136,11 +145,32 @@ private void CheckExcessProperties(TypeInfo.Record actual, TypeInfo expected, Ex
throw new TypeCheckException(
$"Object literal may only specify known properties. " +
$"Excess {(excessKeys.Count == 1 ? "property" : "properties")}: {excessList}",
line: ExcessPropertyLine(sourceExpr, excessKeys[0]),
tsCode: "TS2353"
);
}
}

/// <summary>The source line of an excess property (for TS2353 attribution) — the offending
/// property's own line rather than the enclosing statement's, which matters when the object
/// literal spans multiple lines (e.g. a multi-line call argument). Null if it can't be located.</summary>
private static int? ExcessPropertyLine(Expr sourceExpr, string excessKey)
{
if (sourceExpr is not Expr.ObjectLiteral obj) return null;
foreach (var prop in obj.Properties)
{
if (prop.Key is { } key && GetAccessorMemberName(key) == excessKey)
return key switch
{
Expr.IdentifierKey id => id.Name.Line,
Expr.LiteralKey lit => lit.Literal.Line,
Expr.ComputedKey ck => TryGetExprLine(ck.Expression),
_ => null
};
}
return null;
}

private TypeInfo? GetMemberType(TypeInfo type, string name)
{
if (type is TypeInfo.Record record)
Expand Down
27 changes: 26 additions & 1 deletion TypeSystem/TypeChecker.Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private TypeInfo CheckBinary(Expr.Binary binary)
OperatorDescriptor.Plus => CheckPlusOperator(left, right, line),
OperatorDescriptor.Arithmetic or OperatorDescriptor.Power => CheckArithmeticBinary(left, right, line),
OperatorDescriptor.Comparison => CheckComparisonBinary(left, right, binary.Operator.Lexeme, line),
OperatorDescriptor.Equality => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN),
OperatorDescriptor.Equality => CheckEqualityBinary(left, right, line),
OperatorDescriptor.Bitwise or OperatorDescriptor.BitwiseShift => CheckBitwiseBinary(left, right, line),
OperatorDescriptor.UnsignedRightShift => CheckUnsignedShiftBinary(left, right, line),
OperatorDescriptor.In => CheckInOperator(right, line),
Expand All @@ -51,6 +51,31 @@ private TypeInfo CheckInOperator(TypeInfo right, int line)
return new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN);
}

/// <summary>
/// Equality (<c>==</c>/<c>!=</c>/<c>===</c>/<c>!==</c>). tsc's "no overlap" check (TS2367) is broad,
/// but only the symbol-vs-non-symbol-primitive slice is implemented here (scoped to keep other
/// comparisons untouched): a <c>symbol</c> can never equal a <c>boolean</c>/<c>number</c>/
/// <c>string</c>/<c>bigint</c>, so comparing them is flagged unintentional. A symbol vs symbol / vs
/// <c>any</c> / vs a union that could hold a symbol is fine.
/// </summary>
private TypeInfo CheckEqualityBinary(TypeInfo left, TypeInfo right, int line)
{
bool leftSym = left is TypeInfo.Symbol or TypeInfo.UniqueSymbol;
bool rightSym = right is TypeInfo.Symbol or TypeInfo.UniqueSymbol;
if (leftSym != rightSym)
{
TypeInfo other = leftSym ? right : left;
if (other is TypeInfo.Primitive or TypeInfo.String or TypeInfo.NumberLiteral
or TypeInfo.StringLiteral or TypeInfo.BooleanLiteral or TypeInfo.BigInt or TypeInfo.BigIntLiteral)
{
throw new TypeCheckException(
$"This comparison appears to be unintentional because the types '{left}' and '{right}' have no overlap.",
line > 0 ? line : null, tsCode: "TS2367");
}
}
return new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN);
}

/// <summary>
/// True if this type is Any, Inferred, or a Union containing an Any/Inferred arm.
/// A union that includes 'any' is effectively permissive (JS semantics); operators
Expand Down
18 changes: 17 additions & 1 deletion TypeSystem/TypeChecker.Statements.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,23 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method)
}

// Validate implemented interfaces (skip for generic classes - validated at instantiation)
// Resolved interfaces are captured so an un-annotated method's INFERRED return type can be
// re-checked against them after the body pass (this validation runs before inference).
List<TypeInfo.Interface> implementsForRecheck = [];
if (classStmt.Interfaces != null && classTypeParams == null)
{
for (int i = 0; i < classStmt.Interfaces.Count; i++)
{
var interfaceToken = classStmt.Interfaces[i];
TypeInfo? itfTypeInfo = _environment.Get(interfaceToken.Lexeme);

// #99: a lib wrapper type (`implements String`) resolves to the primitive in type
// position, not an interface — consult the loaded lib for a real interface so it's a
// valid implements target rather than a spurious "not an interface" (TS2304).
if (itfTypeInfo is not TypeInfo.Interface and not TypeInfo.GenericInterface
&& LibTypeLoader.TryGet(interfaceToken.Lexeme, out var libImplements))
itfTypeInfo = libImplements;

// Get type arguments for this interface if provided
List<string>? typeArgs = classStmt.InterfaceTypeArgs != null && i < classStmt.InterfaceTypeArgs.Count
? classStmt.InterfaceTypeArgs[i]
Expand Down Expand Up @@ -613,6 +623,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method)
}

ValidateInterfaceImplementation(classTypeForBody, interfaceType, classStmt.Name.Lexeme, classStmt.Name.Line);
implementsForRecheck.Add(interfaceType);
}
}

Expand Down Expand Up @@ -1106,7 +1117,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method)
// `[Symbol.x]()` method carries its inferred return type rather than the <inferred> placeholder
// it held at the string-index check earlier (ValidateClassPropertiesAgainstIndex).
mutableClass.ResetFrozenCache();
ValidateClassMembersAgainstSymbolIndex(classStmt, mutableClass.Freeze());
var finalizedClass = mutableClass.Freeze();
ValidateClassMembersAgainstSymbolIndex(classStmt, finalizedClass);
// Re-check implemented-interface method compatibility now that un-annotated method return
// types are inferred (the pre-inference pass above saw a <inferred> placeholder — TS2416).
if (implementsForRecheck.Count > 0)
RecheckImplementsInferredReturns(classStmt, finalizedClass, implementsForRecheck);
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions TypeSystem/TypeChecker.Statements.Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt)
var extendType = ResolveAnnotation(
extendTypeName,
interfaceStmt.ExtendsNodes != null && i < interfaceStmt.ExtendsNodes.Count ? interfaceStmt.ExtendsNodes[i] : null)!;
// #99: `interface I extends String` — the wrapper name resolves to the primitive; use
// the loaded lib interface so it's a valid extends target (not a spurious TS2312).
if (extendType is not TypeInfo.Interface && LibTypeLoader.TryGet(extendTypeName, out var libExtends))
extendType = libExtends;
if (extendType is TypeInfo.Interface extendInterface)
{
extendsList.Add(extendInterface);
Expand Down
2 changes: 2 additions & 0 deletions TypeSystem/TypeChecker.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ private void ReportUnknownTypeName(string? annotation, TypeNode? annotationNode,
if (_environment.GetTypeAlias(name) is not null) return;
if (_environment.GetGenericTypeAlias(name) is not null) return;
if (_openTypeVariablesInScope?.Contains(name) == true) return;
// #99 lib-type seam: an ambient lib.d.ts type name (e.g. Symbol/SymbolConstructor) is known.
if (TryResolveLibType(name) is not null) return;

throw new TypeCheckException($" Cannot find name '{name}'.", line, tsCode: "TS2304");
}
Expand Down
28 changes: 28 additions & 0 deletions TypeSystem/TypeChecker.TypeParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,37 @@ private TypeInfo ResolveTypeNameCore(string typeName)
return enumType;
}

// #99 lib-type seam: ambient lib.d.ts TYPE-position names SharpTS doesn't load from the
// .d.ts files yet (checked AFTER user declarations above, so a user interface of the same
// name wins). A later increment backs this with a real lib.d.ts loader.
if (TryResolveLibType(typeName) is { } libType)
{
return libType;
}

return new TypeInfo.Any();
}

/// <summary>
/// The #99 lib-type seam: resolves the ambient TYPE-position names that lib.d.ts declares but
/// SharpTS doesn't yet parse from the vendored .d.ts files, to their modeled <see cref="TypeInfo"/>.
/// Consulted by both <see cref="ResolveTypeNameCore"/> (for the type) and
/// <c>ReportUnknownTypeName</c> (so these names aren't flagged TS2304). Increment 1 covers the
/// Symbol family; a later increment replaces this switch with content loaded from lib.d.ts.
/// </summary>
private static TypeInfo? TryResolveLibType(string name)
{
// Names loaded from the embedded lib.d.ts (e.g. SymbolConstructor) — the real declarations.
if (LibTypeLoader.TryGet(name, out var libType)) return libType;
// Names still hand-modeled (the wrapper types need behavior the loaded shape can't yet give —
// a symbol must satisfy `Symbol` without SharpTS modeling the primitive's apparent members).
return name switch
{
"Symbol" => WellKnownSymbolTypes.SymbolWrapper,
_ => null
};
}

/// <summary>
/// Simplifies an intersection type according to TypeScript semantics:
/// - Conflicting primitives (string &amp; number) = never
Expand Down
Loading