visitClassDeclaration method
Visit a SClassDeclaration.
Implementation
@override
Object? visitClassDeclaration(SClassDeclaration node) {
final className = node.name!.name;
Logger.debug(
"[Visitor.visitClassDeclaration] START for '$className' in env: ${environment.hashCode}",
);
// Retrieve the placeholder class object created in Pass 1
Object? placeholder = environment.get(className);
if (placeholder == null || placeholder is! InterpretedClass) {
// This should not happen if Pass 1 worked correctly
throw StateD4rtException(
"Placeholder for class '$className' not found or invalid during Pass 2.",
);
}
final klass = placeholder;
Logger.debug(
"[Visitor.visitClassDeclaration] Retrieved placeholder for '$className' (hash: ${klass.hashCode})",
);
// Resolve and populate relationships ON THE EXISTING klass object
// Superclass lookup
// InterpretedClass? superclass; // Keep this commented or remove
if (node.extendsClause != null) {
final superclassName = node.extendsClause!.superclass!.name!.name;
Logger.debug(
"[Visitor.visitClassDeclaration] Trying to get superclass '$superclassName' from env: ${environment.hashCode}",
);
Object? potentialSuperclass;
try {
potentialSuperclass = environment.get(superclassName);
} on RuntimeD4rtException {
throw RuntimeD4rtException(
"Superclass '$superclassName' not found for class '$className'. Ensure it's defined.",
);
}
if (potentialSuperclass is InterpretedClass) {
// Standard Dart Superclass
final superclass = potentialSuperclass;
// Add checks for final and interface modifiers
// Note: sealed and interface classes CAN be extended within the same library/file,
// and in D4rt all interpreted code is considered the same library
//
// For 'final' classes:
// - A 'final' class cannot be extended outside its library
// - An 'abstract final' class CAN be extended within the same library (needs implementation)
// - Since all D4rt code is considered the same library, we only block non-abstract final classes
if (superclass.isFinal && !superclass.isAbstract) {
throw RuntimeD4rtException(
"Class '$className' cannot extend final class '$superclassName'.",
);
}
// Interface classes can be extended within the same library, so we allow it in D4rt
// sealed classes are also allowed to be extended - they restrict external libraries only
// Set the superclass and clear bridged superclass
klass.superclass = superclass;
klass.bridgedSuperclass = null;
Logger.debug(
"[Visitor.visitClassDeclaration] Set standard superclass '$superclassName' for '$className'",
);
} else if (potentialSuperclass is BridgedClass) {
final bridgedSuperclass = potentialSuperclass;
// No modifier checks needed for bridged classes (yet?)
// Set the bridged superclass and clear standard superclass
klass.bridgedSuperclass = bridgedSuperclass;
klass.superclass = null;
// Capture the reified type arguments from the extends clause so
// proxy factories can pick the right typed variant (e.g.
// `extends CustomClipper<RRect>` → pick the RRect proxy, not the
// default Path one). See [InterpretedClass.bridgedSuperTypeArgNames].
final superTypeArgs =
node.extendsClause!.superclass!.typeArguments?.arguments;
if (superTypeArgs != null && superTypeArgs.isNotEmpty) {
klass.bridgedSuperTypeArgNames = superTypeArgs
.map((arg) => arg is SNamedType ? (arg.name?.name ?? '') : '')
.toList();
}
Logger.debug(
"[Visitor.visitClassDeclaration] Set BRIDGED superclass '$superclassName' for '$className' (typeArgs=${klass.bridgedSuperTypeArgNames})",
);
} else {
throw RuntimeD4rtException(
"Superclass '$superclassName' for class '$className' resolved to ${potentialSuperclass?.runtimeType}, which is not a class or bridged class.",
);
}
}
// Interface lookup
if (node.implementsClause != null) {
Logger.debug(
"[Visitor.visitClassDeclaration] Processing 'implements' clause for '$className' in env: ${environment.hashCode}",
);
for (final interfaceType in node.implementsClause!.interfaces) {
final interfaceName = interfaceType.name!.name;
Logger.debug(
"[Visitor.visitClassDeclaration] Trying to get interface '$interfaceName' from env: ${environment.hashCode}",
);
try {
final potentialInterface = environment.get(interfaceName);
if (potentialInterface is InterpretedClass) {
// Add checks for base modifier only
// Note: sealed classes CAN be implemented within the same library/file
if (potentialInterface.isBase) {
throw RuntimeD4rtException(
"Class '$className' cannot implement base class '$interfaceName' outside of its library.",
);
}
// sealed classes are allowed to be implemented - they restrict external libraries only
// Add to the interfaces list of the existing klass object
klass.interfaces.add(potentialInterface);
Logger.debug(
"[Visitor.visitClassDeclaration] Added interface '$interfaceName' for '$className'",
);
} else if (potentialInterface is BridgedClass) {
// Support for bridged interfaces like Comparable, Exception
klass.bridgedInterfaces.add(potentialInterface);
Logger.debug(
"[Visitor.visitClassDeclaration] Added bridged interface '$interfaceName' for '$className'",
);
} else {
throw RuntimeD4rtException(
"Class '$className' cannot implement non-class '$interfaceName' (${potentialInterface?.runtimeType}).",
);
}
} on RuntimeD4rtException {
throw RuntimeD4rtException(
"Interface '$interfaceName' not found for class '$className'. Ensure it's defined.",
);
}
}
}
// Mixin application lookup
if (node.withClause != null) {
Logger.debug(
"[Visitor.visitClassDeclaration] Processing 'with' clause for '$className' in env: ${environment.hashCode}",
);
for (final mixinType in node.withClause!.mixinTypes) {
final mixinName = mixinType.name!.name;
Logger.debug(
"[Visitor.visitClassDeclaration] Trying to get mixin '$mixinName' from env: ${environment.hashCode}",
);
Object? mixin;
try {
mixin = environment.get(mixinName);
} on RuntimeD4rtException {
throw RuntimeD4rtException(
"Mixin '$mixinName' not found during lookup for class '$className'. Ensure it's defined (as a mixin or class mixin).",
);
}
if (mixin is InterpretedClass) {
if (!mixin.isMixin) {
throw RuntimeD4rtException(
"Class '$mixinName' cannot be used as a mixin because it's not declared with 'mixin' or 'class mixin'.",
);
}
// Add checks for base modifier only
// Note: sealed classes CAN be mixed in within the same library/file
if (mixin.isBase) {
throw RuntimeD4rtException(
"Class '$className' cannot mix in base class '$mixinName' outside of its library.",
);
}
// sealed classes are allowed to be mixed in - they restrict external libraries only
// Add to the mixins list of the existing klass object
klass.mixins.add(mixin);
Logger.debug(
"[Visitor.visitClassDeclaration] Applied interpreted mixin '$mixinName' to '$className'",
);
} else if (mixin is BridgedClass) {
// Support for bridged classes as mixins
if (!mixin.canBeUsedAsMixin) {
throw RuntimeD4rtException(
"Bridged class '$mixinName' cannot be used as a mixin. Set canBeUsedAsMixin=true when registering the bridge.",
);
}
// Add to the bridged mixins list
klass.bridgedMixins.add(mixin);
Logger.debug(
"[Visitor.visitClassDeclaration] Applied bridged mixin '$mixinName' to '$className'",
);
} else {
throw RuntimeD4rtException(
"Identifier '$mixinName' resolved to ${mixin?.runtimeType}, which is not a class/mixin, for class '$className'.",
);
}
}
}
// Populate members ON THE EXISTING klass object
final staticInitEnv = environment; // Statics use the class definition env
final originalVisitorEnv = environment; // Backup visitor env
Logger.debug(
"[Visitor.visitClassDeclaration] Processing members for '$className' (hash: ${klass.hashCode})",
);
for (final member in node.members) {
if (member is SMethodDeclaration) {
final methodName = member.name!.name;
// Pass the ALREADY RETRIEVED klass object
final function = InterpretedFunction.method(
member,
staticInitEnv,
klass,
);
if (member.isStatic) {
if (member.isGetter) {
klass.staticGetters[methodName] = function;
} else if (member.isSetter) {
klass.staticSetters[methodName] = function;
} else {
klass.staticMethods[methodName] = function;
}
} else if (!member.isAbstract) {
if (member.isGetter) {
klass.getters[methodName] = function;
} else if (member.isSetter) {
klass.setters[methodName] = function;
} else if (member.isOperator) {
// Add operator methods to the operators map
klass.operators[methodName] = function;
} else {
klass.methods[methodName] = function;
}
} else {
// Abstract
if (!klass.isAbstract) {
throw RuntimeD4rtException(
"Abstract methods can only be declared in abstract classes. Method '${klass.name}.$methodName'.",
);
}
if (member.body is! SEmptyFunctionBody) {
throw RuntimeD4rtException(
"Abstract methods cannot have a body. Method '${klass.name}.$methodName'.",
);
}
if (member.isGetter) {
klass.getters[methodName] = function;
} else if (member.isSetter) {
klass.setters[methodName] = function;
} else if (member.isOperator) {
// Add abstract operator methods to the operators map
klass.operators[methodName] = function;
} else {
klass.methods[methodName] = function;
}
}
} else if (member is SConstructorDeclaration) {
final function = InterpretedFunction.constructor(
member,
staticInitEnv,
klass,
);
final constructorName = member.name?.name ?? '';
klass.constructors[constructorName] = function;
} else if (member is SFieldDeclaration) {
// Defer static field processing - collect them for later
if (!member.isStatic) {
klass.fieldDeclarations.add(member);
}
} else {
throw StateD4rtException('Unknown member type: ${member.runtimeType}');
}
}
// TWO-PASS STATIC FIELD PROCESSING to handle sibling references (Bug-23)
// Pass 1: Register all static field names in the class environment with null values
final staticFieldDecls = <SVariableDeclaration>[];
final staticFieldMeta =
<String, (bool isLate, bool isFinal, SAstNode? initializer)>{};
for (final member in node.members) {
if (member is SFieldDeclaration && member.isStatic) {
final isLate = member.fields!.isLate;
final isFinal = member.fields!.isFinal;
for (final variable in member.fields!.variables) {
final fieldName = variable.name!.name;
staticFieldDecls.add(variable);
staticFieldMeta[fieldName] = (isLate, isFinal, variable.initializer);
// Pre-register all static fields so they can reference each other
klass.staticFields[fieldName] = null;
}
}
}
// Pass 2: Evaluate all static field initializers (now all fields are known)
// Create a child environment that can shadow with evaluated field values.
//
// Bug-43 / forward-class-reference FIX: evaluating `static const`
// initializers can require other classes (and their constructors) that
// appear later in source order. We support deferring this whole block
// to after every class has been visited.
final staticFieldEvalEnv = Environment(enclosing: staticInitEnv);
void evaluateStaticFields() {
final savedEnvOnEntry = environment;
try {
environment = staticFieldEvalEnv;
for (final variable in staticFieldDecls) {
final fieldName = variable.name!.name;
final (isLate, isFinal, initializer) = staticFieldMeta[fieldName]!;
if (isLate) {
// Handle late static field
if (initializer != null) {
// Late static field with lazy initializer
final lateVar = LateVariable(fieldName, () {
// Closure evaluates the initializer on first access.
final savedEnv = environment;
try {
environment = staticFieldEvalEnv;
return initializer.accept<Object?>(this);
} finally {
environment = savedEnv;
}
}, isFinal: isFinal);
klass.staticFields[fieldName] = lateVar;
// Also define in eval environment for sibling access
staticFieldEvalEnv.define(fieldName, lateVar);
Logger.debug(
"[ClassDecl] Defined late static field '$fieldName' with lazy initializer for class '${klass.name}'.",
);
} else {
// Late static field without initializer
final lateVar = LateVariable(fieldName, null, isFinal: isFinal);
klass.staticFields[fieldName] = lateVar;
staticFieldEvalEnv.define(fieldName, lateVar);
Logger.debug(
"[ClassDecl] Defined late static field '$fieldName' without initializer for class '${klass.name}'.",
);
}
} else {
// Regular static field handling
Object? value;
if (initializer != null) {
value = initializer.accept<Object?>(this);
}
klass.staticFields[fieldName] = value;
// Define the evaluated value in the environment so subsequent fields can access it
staticFieldEvalEnv.define(fieldName, value);
}
}
} finally {
environment = savedEnvOnEntry;
}
}
if (deferStaticFieldInits) {
Logger.debug(
"[ClassDecl] Deferring static-field initialization for class '${klass.name}' until all classes are declared.",
);
_pendingStaticInitializers.add(evaluateStaticFields);
// Leave environment where the caller expects it (originalVisitorEnv).
environment = originalVisitorEnv;
} else {
try {
evaluateStaticFields();
} finally {
environment = originalVisitorEnv;
}
}
Logger.debug(
"[Visitor.visitClassDeclaration] Finished processing members for '$className'",
);
// Final Checks (run on the populated klass object)
// Check for unimplemented abstract members
if (!klass.isAbstract) {
final inheritedAbstract = klass.getAbstractInheritedMembers();
// G-DOV-6/7 FIX: Use getAllConcreteMembers() to walk the full superclass chain
// (not just this class + mixins), so grandparent concrete implementations are found.
final concreteMembers = klass.getAllConcreteMembers();
final fieldNames = klass
.getInstanceFieldNames(); // Fields also satisfy abstract getters
for (final abstractName in inheritedAbstract.keys) {
// Check if the abstract member is satisfied by a concrete method/getter/setter OR a field
if (!concreteMembers.containsKey(abstractName) &&
!fieldNames.contains(abstractName)) {
final abstractMember = inheritedAbstract[abstractName]!;
String memberType = "method";
if (abstractMember.isGetter) memberType = "getter";
if (abstractMember.isSetter) memberType = "setter";
throw RuntimeD4rtException(
"Missing concrete implementation for inherited abstract $memberType '$abstractName' in class '${klass.name}'.",
);
}
}
}
// Check for unimplemented interface members
if (!klass.isAbstract) {
final requiredInterfaceMembers = klass.getAllInterfaceMembers();
final availableConcreteMembers = klass.getAllConcreteMembers();
for (final requiredName in requiredInterfaceMembers.keys) {
if (!availableConcreteMembers.containsKey(requiredName)) {
final memberType = requiredInterfaceMembers[requiredName]!;
throw RuntimeD4rtException(
"Missing concrete implementation for interface $memberType '$requiredName' in class '${klass.name}'.",
);
}
}
}
// Perf T6: all function tables are fully populated; compact them into
// their read-only form. staticFields is excluded (deferred init).
klass.freezeMemberTables();
Logger.debug("[Visitor.visitClassDeclaration] END for '$className'");
// No need to define/assign klass in the environment here, it was done in Pass 1.
return null; // Class declaration statement doesn't return a value
}