Skip to content

Commit 6dd6b30

Browse files
antonsyndclaude
andcommitted
refactor: remove collections.namedtuple, add helpful SPY0432 error (#508)
collections.namedtuple is redundant with Sharpy's native named tuples (`type Point = tuple[x: float, y: float]`) and @DataClass, while being strictly inferior — untyped fields (object), different C# target (record vs ValueTuple), and string-based field names violating Axiom 3. Remove the entire implementation: - Delete Sharpy.Core/Collections/NamedTuple.cs runtime stub - Remove TryCheckNamedTupleDefinition from TypeChecker - Remove SemanticInfo namedtuple tracking (MarkAsNamedTupleDefinition) - Remove GenerateNamedTupleRecord from RoslynEmitter - Remove ModuleLevelValidator namedtuple exemption - Remove ISemanticQuery.GetNamedTupleDefinition Add SPY0432 error in ImportResolver that fires on `from collections import namedtuple` with a message suggesting native alternatives. Convert match_named_tuple fixture from .expected to .error test. Delete namedtuple_basic and namedtuple_print fixtures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29aef76 commit 6dd6b30

15 files changed

Lines changed: 51 additions & 302 deletions

File tree

src/Sharpy.Compiler.Tests/Integration/TestFixtures/expressions/namedtuple_basic.expected

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/Sharpy.Compiler.Tests/Integration/TestFixtures/expressions/namedtuple_basic.spy

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/Sharpy.Compiler.Tests/Integration/TestFixtures/expressions/namedtuple_print.expected

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/Sharpy.Compiler.Tests/Integration/TestFixtures/expressions/namedtuple_print.spy

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
collections.namedtuple is not supported in Sharpy

src/Sharpy.Compiler.Tests/Integration/TestFixtures/pattern_matching/match_named_tuple.expected

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/Sharpy.Compiler/CodeGen/RoslynEmitter.ModuleClass.cs

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -502,94 +502,11 @@ private MemberDeclarationSyntax GenerateReExportProperty(string localName, Varia
502502
VariableDeclaration varDecl => GenerateModuleLevelField(varDecl),
503503
TypeAlias => null, // Type aliases are compile-time only, no C# output
504504
ReturnStatement ret => GenerateReturn(ret),
505-
Assignment assign when _context.SemanticInfo?.GetNamedTupleDefinition(assign) is TypeSymbol ntSym
506-
=> GenerateNamedTupleRecord(ntSym),
507505
Assignment assign => GenerateAssignment(assign),
508506
ImportStatement => null, // Imports are resolved at semantic level, no C# output
509507
FromImportStatement => null, // Imports are resolved at semantic level, no C# output
510508
_ => EmitUnrecognizedStatementDiagnostic(stmt)
511509
};
512510
}
513511

514-
/// <summary>
515-
/// Generates a C# record class for a namedtuple definition.
516-
/// Example: Point = namedtuple("Point", ["x", "y"]) generates:
517-
/// public record Point(object X, object Y)
518-
/// {
519-
/// public override string ToString() => $"Point(x={X}, y={Y})";
520-
/// }
521-
/// </summary>
522-
private RecordDeclarationSyntax GenerateNamedTupleRecord(TypeSymbol typeSymbol)
523-
{
524-
var typeName = NameMangler.Transform(typeSymbol.Name, NameContext.Type);
525-
526-
// Generate primary constructor parameters from fields
527-
var parameters = typeSymbol.Fields.Select(field =>
528-
{
529-
var paramType = _typeMapper.MapSemanticType(field.Type ?? SemanticType.Object);
530-
var paramName = NameMangler.Transform(field.Name, NameContext.Field);
531-
return Parameter(Identifier(paramName)).WithType(paramType);
532-
}).ToArray();
533-
534-
var parameterList = ParameterList(SeparatedList(parameters));
535-
536-
// Build ToString() override using interpolated string: $"Point(x={X}, y={Y})"
537-
var toStringMethod = MethodDeclaration(
538-
PredefinedType(Token(SyntaxKind.StringKeyword)),
539-
"ToString")
540-
.WithModifiers(TokenList(
541-
Token(SyntaxKind.PublicKeyword),
542-
Token(SyntaxKind.OverrideKeyword)))
543-
.WithExpressionBody(ArrowExpressionClause(
544-
InterpolatedStringExpression(Token(SyntaxKind.InterpolatedStringStartToken))
545-
.WithContents(BuildNamedTupleToStringContents(typeSymbol))))
546-
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
547-
548-
var record = RecordDeclaration(Token(SyntaxKind.RecordKeyword), typeName)
549-
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
550-
.WithParameterList(parameterList)
551-
.WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken))
552-
.WithMembers(SingletonList<MemberDeclarationSyntax>(toStringMethod))
553-
.WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken));
554-
555-
return record;
556-
}
557-
558-
/// <summary>
559-
/// Builds the interpolated string contents for namedtuple ToString():
560-
/// "Point(x={X}, y={Y})" using SyntaxFactory nodes.
561-
/// </summary>
562-
private static SyntaxList<InterpolatedStringContentSyntax> BuildNamedTupleToStringContents(
563-
TypeSymbol typeSymbol)
564-
{
565-
var contents = new List<InterpolatedStringContentSyntax>();
566-
567-
// Leading text: "TypeName("
568-
contents.Add(InterpolatedStringText(
569-
Token(TriviaList(), SyntaxKind.InterpolatedStringTextToken,
570-
$"{typeSymbol.Name}(", $"{typeSymbol.Name}(", TriviaList())));
571-
572-
for (int i = 0; i < typeSymbol.Fields.Count; i++)
573-
{
574-
var field = typeSymbol.Fields[i];
575-
var originalName = field.Name;
576-
var propertyName = NameMangler.Transform(field.Name, NameContext.Field);
577-
578-
// "x=" prefix (with ", " separator for non-first fields)
579-
var prefix = i > 0 ? $", {originalName}=" : $"{originalName}=";
580-
contents.Add(InterpolatedStringText(
581-
Token(TriviaList(), SyntaxKind.InterpolatedStringTextToken,
582-
prefix, prefix, TriviaList())));
583-
584-
// {PropertyName} interpolation
585-
contents.Add(Interpolation(IdentifierName(propertyName)));
586-
}
587-
588-
// Closing ")"
589-
contents.Add(InterpolatedStringText(
590-
Token(TriviaList(), SyntaxKind.InterpolatedStringTextToken,
591-
")", ")", TriviaList())));
592-
593-
return List(contents);
594-
}
595512
}

src/Sharpy.Compiler/Diagnostics/DiagnosticCodes.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ public static class Validation
417417
// Access modifier decorator validation (SPY0430-SPY0431)
418418
public const string ConflictingAccessModifiers = "SPY0430"; // Active
419419
public const string AccessModifierOnDunder = "SPY0431"; // Active
420-
// SPY0432-SPY0449: Reserved for future validation errors
420+
public const string NamedtupleNotSupported = "SPY0432"; // Active
421+
// SPY0433-SPY0449: Reserved for future validation errors
421422

422423
#endregion
423424

src/Sharpy.Compiler/Diagnostics/DiagnosticExplanations.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,17 @@ private static Dictionary<string, DiagnosticExplanation> BuildExplanations()
13411341
"class Foo:\n @private\n def __init__(self) -> None:\n ...",
13421342
"Remove the access modifier decorator from the dunder method:\nclass Foo:\n def __init__(self) -> None:\n ...");
13431343

1344+
// ── Validation errors: Unsupported Python constructs (SPY0432) ──
1345+
1346+
Add(dict, DiagnosticCodes.Validation.NamedtupleNotSupported,
1347+
"collections.namedtuple is not supported",
1348+
"Validation",
1349+
"Sharpy does not support collections.namedtuple. Use native named tuples " +
1350+
"(type aliases with named fields) or @dataclass for data-holding classes instead.",
1351+
"from collections import namedtuple\nPoint = namedtuple(\"Point\", [\"x\", \"y\"])",
1352+
"Use native named tuples:\ntype Point = tuple[x: float, y: float]\n\n" +
1353+
"Or use @dataclass:\n@dataclass\nclass Point:\n x: float\n y: float");
1354+
13441355
// ── Validation warnings: Deprecation (SPY0464) ─────────────────
13451356

13461357
Add(dict, DiagnosticCodes.Validation.DeprecatedBodylessSyntax,

src/Sharpy.Compiler/Semantic/ImportResolver.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,43 @@ public void ResolveAllImports(Module module, SymbolTable symbolTable, string? cu
338338
_logger.LogDebug($"[ImportResolver] Current module: {Path.GetFileName(_currentModulePath)}");
339339
}
340340

341+
// Helpful error for unsupported Python constructs: intercept before module resolution
342+
// so the error fires even when the module has no remaining exported functions.
343+
if (fromImport.Module == "collections" && !fromImport.ImportAll)
344+
{
345+
foreach (var alias in fromImport.Names)
346+
{
347+
if (alias.Name == "namedtuple")
348+
{
349+
AddError(
350+
"collections.namedtuple is not supported in Sharpy. " +
351+
"Use 'type Point = tuple[x: float, y: float]' for named tuples, " +
352+
"or '@dataclass class Point: x: float; y: float' for data classes.",
353+
alias.LineStart, alias.ColumnStart,
354+
code: DiagnosticCodes.Validation.NamedtupleNotSupported,
355+
span: alias.Span ?? fromImport.Span);
356+
357+
// Create error recovery module to suppress cascading errors
358+
var errorRecoveryModule = CreateErrorRecoveryModule(
359+
fromImport.Module, fromImport.LineStart, fromImport.ColumnStart);
360+
foreach (var importAlias in fromImport.Names)
361+
{
362+
var targetName = importAlias.AsName ?? importAlias.Name;
363+
errorRecoveryModule.Exports[targetName] = CreateErrorRecoverySymbol(
364+
targetName, fromImport.Module, importAlias.LineStart, importAlias.ColumnStart);
365+
_diagnostics.MarkAsRootCause(targetName);
366+
}
367+
return new ModuleInfo
368+
{
369+
Path = $"<error-recovery:{fromImport.Module}>",
370+
Module = null!,
371+
ExportedSymbols = errorRecoveryModule.Exports,
372+
IsNetModule = false
373+
};
374+
}
375+
}
376+
}
377+
341378
// First, try to resolve as .NET assembly module
342379
var moduleInfo = TryResolveNetModule(fromImport.Module, fromImport.LineStart, fromImport.ColumnStart);
343380

0 commit comments

Comments
 (0)