Tom Coding Guidelines

Guidelines derived from the reflection, observable, and timezoned modules for consistent, maintainable code.

Table of Contents

---

Code Structure

1. File Organization

Files should follow a consistent structure:

// 1. Ignore directives (if needed)
// ignore_for_file: type_literal_in_constant_pattern

// 2. Imports - grouped by: dart:, package:, relative
import 'dart:convert';
import 'package:tom_reflection/tom_reflection.dart';
import '../little_things/exception_base.dart';

// 3. Exports (if providing a barrel file)
export '../reflection/reflection.dart';

// 4. Exception classes (if any)
class TomReflectorException extends TomException { ... }

// 5. Main classes - public API first
class TomReflectionInfo { ... }

// 6. Global constants/instances - `final`, so a consumer cannot swap them out
const TomReflector tomReflector = TomReflector();
final TomReflectionInfo tomReflectionInfo = TomReflectionInfo();

2. Class Organization

Within a class, organize members in this order:

class ExampleClass {
  // 1. Static constants and fields
  static const String serializationPrefix = "@X@";
  static bool debugSwitchLogDetails = false;
  
  // 2. Static final fields (singletons, handlers)
  static final Map<Type, Function> _handlers = { ... };
  
  // 3. Instance fields (public, then private)
  final String name;
  TomTimezone _timezone = TomTimezone.utc;
  
  // 4. Getters and setters
  TomTimezone get timezone => _timezone;
  
  // 5. Constructors (default, named, factory - in that order)
  ExampleClass(this.name);
  ExampleClass.utc({ ... });
  factory ExampleClass.fromDateTime(DateTime dt) { ... }
  
  // 6. Public methods (grouped by functionality)
  // Use section comments to separate groups:
  // ---------------------------------------------------------------------------
  // Instance Creation Methods
  // ---------------------------------------------------------------------------
  T createInstance<T>() { ... }
  
  // 7. Private helper methods
  void _logDetails() { ... }
  
  // 8. Operator overloads
  @override
  String toString() { ... }
}

3. Section Comments

Use consistent section dividers for grouping related methods:

// ---------------------------------------------------------------------------
// Type Resolution Methods
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Value Setting Methods (Deserialization)
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Value Getting Methods (Serialization)
// ---------------------------------------------------------------------------

---

Method Design

1. Maximum Method Length

Target: 15-25 lines of executable code

Methods exceeding 25 lines should be broken into smaller helper methods. The observed codebase follows this pattern:

Method TypeTarget LinesMax Lines
Public API methods10-2030
Private helpers5-1520
Factory constructors10-1520
Simple getters/setters1-35

2. Complexity Guidelines

Cyclomatic Complexity Target: ≤ 7

  • 1-4: Simple method, ideal
  • 5-7: Moderate complexity, acceptable
  • 8-10: Consider refactoring
  • 10+: Refactor immediately

3. Method Decomposition Pattern

Large methods should delegate to small, focused helpers:

// Good: Main method delegates to helpers
void _setFieldValue(InstanceMirror im, ClassMirror cm, MapEntry<String, dynamic> entry) {
  if (_trySetPrimitiveValue(im, entry)) return;
  
  final imGet = cm.instanceMembers[entry.key];
  final imSet = cm.instanceMembers["${entry.key}="];
  
  if (imGet != null && imSet != null) {
    _setComplexValue(im, cm, imGet, entry);
  } else {
    _handleMissingField(imGet, entry);
  }
}

// Each helper is focused and small
bool _trySetPrimitiveValue(InstanceMirror im, MapEntry<String, dynamic> entry) {
  if (entry.value == null || entry.value is int || entry.value is double ||
      entry.value is bool || entry.value is String) {
    im.invokeSetter(entry.key, entry.value);
    return true;
  }
  return false;
}

4. Try-Pattern Methods

For operations that may fail gracefully, use the _try* pattern:

bool _trySetPrimitiveValue(...) { ... }      // Returns bool
bool _trySetPrimitiveMapType(...) { ... }    // Returns bool
Map<String, Object?>? _tryGetJsonEncode(...) { ... }  // Returns nullable

5. Method Ordering

Order methods by their call sequence:

// Entry point method
T setValues<T>(T o, Map<String, dynamic> values) {
  // 1. Check for TomClass
  if (o is TomClass) return o.setValues(values) as T;
  
  // 2. Try jsonDecode
  if (_tryInvokeJsonDecode(im, values)) return o;
  
  // 3. Use reflection
  for (var entry in values.entries) {
    _setFieldValue(im, cm, entry);
  }
  return o;
}

// Helper methods follow in call order
bool _tryInvokeJsonDecode(...) { ... }
void _setFieldValue(...) { ... }
bool _trySetPrimitiveValue(...) { ... }
void _setComplexValue(...) { ... }
// etc.

---

Documentation Standards

1. Class Documentation

Every public class needs comprehensive documentation:

/// A timezone-aware datetime value.
///
/// [TomZonedDateTime] represents a complete date and time associated with
/// a specific timezone. This is the most commonly used timezone-aware type.
///
/// ## Internal Representation
///
/// The datetime is stored internally as UTC. When you create a TomZonedDateTime
/// with local time values, they are converted to UTC for storage.
///
/// ## Serialization
///
/// Serializes with the `@X@` prefix:
/// ```
/// @X@2024-06-15T05:30:00.000Z_+09:00_Asia/Tokyo
/// ```
///
/// ## Usage
///
/// ```dart
/// final tokyo = TomTimezone.fromName('Asia/Tokyo');
/// final dt = TomZonedDateTime(
///   year: 2024, month: 6, day: 15, hour: 14, minute: 30,
///   timezone: tokyo,
/// );
/// print(dt.localHour); // 14 (local time in Tokyo)
/// ```
class TomZonedDateTime extends TomTimezoned { ... }

Required sections for main classes: - One-line summary - Detailed description paragraph - ## Internal Representation (if non-obvious) - ## Usage or ## Example with code - ## Serialization (if applicable)

2. Method Documentation

Public Methods

/// Creates a new instance of type [T] using its default constructor.
///
/// The type must be annotated with `@tomReflector` and have a default
/// (no-argument) constructor.
///
/// Example:
/// ```dart
/// final person = tomReflectionInfo.createInstance<Person>();
/// // Or with explicit type:
/// final person = tomReflectionInfo.createInstance(Person);
/// ```
///
/// Throws [TomReflectorException] if instantiation fails.
T createInstance<T extends Object>([Type? type]) { ... }

Required elements: - One-line summary - Parameter descriptions (if not obvious) - Example code (for non-trivial methods) - Return value description - Exception documentation (Throws)

Private Methods

Private methods need lighter documentation:

/// Attempts to set a primitive value directly via setter invocation.
///
/// Returns true if the value was successfully set, false otherwise.
bool _trySetPrimitiveValue(InstanceMirror im, MapEntry<String, dynamic> entry) { ... }

3. Parameter Documentation

Use bracketed references for parameters:

/// Creates a [TomZonedDateTime] with specified components in local timezone.
///
/// [year] The year in local time (defaults to 1970).
/// [month] The month 1-12 in local time (defaults to 1).
/// [day] The day 1-31 in local time (defaults to 1).
/// [timezone] The timezone for the local time values (defaults to UTC).
factory TomZonedDateTime({
  int year = 1970,
  int month = 1,
  int day = 1,
  TomTimezone? timezone,
}) { ... }

4. Code Example Standards

/// Example:
/// ```dart
/// final tokyo = TomTimezone.fromName('Asia/Tokyo')!;
/// final meeting = TomZonedDateTime(
///   year: 2024, month: 6, day: 15, hour: 14, minute: 30,
///   timezone: tokyo,
/// );
/// print(meeting.localHour); // 14
/// print(meeting.hour);      // 5 (UTC)
/// ```

Requirements: - Use realistic, meaningful values - Include expected output as comments - Keep examples focused (3-10 lines) - Use deterministic values (avoid DateTime.now() in examples)

5. Property/Field Documentation

/// The IANA timezone name (e.g., 'America/New_York', 'Europe/London').
final String timezone;

/// When true, logs detailed member information during reflection operations.
static bool debugSwitchLogReflectionDetailsMembers = false;

/// Returns the serialization prefix for this type.
///
/// The base class returns an empty string. Subclasses override this
/// to provide type-specific prefixes:
/// - [TomZonedDate]: `@D@`
/// - [TomZonedTime]: `@T@`
/// - [TomZonedDateTime]: `@X@`
String getSerializationPrefix() => '';

6. Section Documentation in Markdown Files

Maintain separate .md documentation files for each module with:

1. Table of Contents 2. Overview - What the module does 3. Quick Start - Minimal working example 4. Core Components - Main classes and their roles 5. Detailed Sections - Deep dives into each feature 6. Best Practices - Usage recommendations 7. Error Handling - Common errors and solutions

---

Naming Conventions

1. Class Names

TypePatternExamples
Base classes Tom + concept TomObservable, TomTimezoned
Observable wrappers Tom + type TomString, TomInt, TomDateTime
Observable nullable TomN + type TomNString, TomNInt
Observable timezoned TomO + zoned type TomOZonedDate, TomOZonedDateTime
Nullable timezoned TomNO + zoned type TomNOZonedDate
Exceptions Concept + Exception TomReflectorException, TomTimezoneException
Utility classesConcept + InfoTomReflectionInfo

2. Method Names

PatternPurposeExamples
get* Retrieve computed value getValues(), getMembers()
set* Set value(s) setValues(), setByString()
create* Create new instance createInstance(), createInstanceFromJson()
convert* Transform data convertToTzSameInstant(), convertToJsonString()
parse*Parse from stringparseFromString()
is* Boolean check (static) isDate(), isTomZonedDateTimeString()
_try* May fail gracefully _trySetPrimitiveValue(), _tryGetJsonEncode()
_handle* Handle specific case _handleMissingField()
_attach* / _detach* Observer management _attachTo(), _detachFrom()

3. Field Names

// Private backing fields
TomTimezone _timezone = TomTimezone.utc;
late T _value;
bool _silent = false;
bool _dirty = false;

// Static handlers/registries (private)
static final Map<Type, Function> _primitiveMapTypeHandlers = { ... };
static final Map<String, TomTimezone> _timezonesMap = {};

// Debug switches (public static)
static bool debugSwitchLogReflectionDetailsMembers = false;

// Serialization constants
static const String serializationPrefix = "@X@";

4. Parameter Names

Use descriptive, contextual names:

// Good
factory TomZonedDateTime.fromDateTime(DateTime dtx, [TomTimezone? timezone])
void setValues(Map<String, dynamic> values)
T createInstanceFromJson<T>(ClassMirror objectMirror, Map<String, Object?> jsonData)

// Avoid
void setValues(Map<String, dynamic> m)  // Too short
T createInstanceFromJson<T>(ClassMirror cm, Map<String, Object?> d)  // Cryptic

5. CLI Argument Naming

When implementing CLI tools, adhere to the following naming conventions for command-line inputs:

  • Arguments: Do not start with a dash (e.g., project_name).
  • Parameters: Start with a single dash (-) and must use -key=value format (with equals sign).
  • Options: Start with a single or double dash (- or --) and are boolean flags (e.g., -verbose).

---

Error Handling

1. Exception Class Pattern

/// Exception thrown when reflection operations fail.
///
/// This exception is used throughout the reflection system to indicate
/// errors during:
/// - Instance creation from JSON
/// - Field value setting via reflection
/// - Type resolution for collections
class TomReflectorException extends TomException {
  TomReflectorException(
    super.key,
    super.defaultUserMessage, {
    super.parameters,
    super.stack,
    super.rootException,
    super.autoLog,
  });
}

2. Exception Key Naming

Use hierarchical, descriptive keys:

// Pattern: module.operation.specific_error
"reflector.instance_creation.failed"
"reflector.classmirror.not_found_for_map_value"
"reflection.set.unexpected_type"
"reflection.set.datetime_must_be_filled_from_string_in_map"
"observable.class_state.set_not_allowed"
"timezone.not_found.name_offset_mismatch"

All three segments are lowercase, and the first segment names the module — never a class. That is what keeps keys from unrelated code from colliding, and it is the grammar TomException itself documents. When several types in one module raise similar errors, distinguish them in the operation segment (observable.class_state.…, observable.list_state.…, observable.map_state.…), not by promoting the type into the module slot.

3. Exception Throwing Pattern

throw TomReflectorException(
  "reflector.instance_creation.failed",
  "Failed to create instance of type ${type ?? T}: $e",
  autoLog: true,
  rootException: e,
  stack: s,
);

Include: - Unique error key - Human-readable message with context - Original exception (rootException) - Stack trace when available - autoLog: true for unexpected errors

4. Graceful Failure Pattern

For operations that may fail but have fallbacks:

T? createInstanceFromJson<T>(ClassMirror objectMirror, Map<String, Object?> jsonData) {
  // Try preferred method first
  try {
    return objectMirror.newInstance("fromJson", [jsonData]) as T;
  } catch (error) {
    // Silently fall through to alternative
  }
  
  // Fallback to default constructor + setValues
  T result = objectMirror.newInstance("", []) as T;
  return setValues<T>(result, jsonData);
}

5. Validation Before Parsing

factory TomZonedDateTime.parseFromString(String formattedString) {
  // Validate format first
  if (!TomZonedDateTime.isTomZonedDateTimeString(formattedString)) {
    throw FormatException("Invalid TomZonedDateTime string: $formattedString");
  }
  
  // Parse with confidence
  final match = RegExp(...).firstMatch(formattedString.trim());
  if (match == null) {
    throw FormatException("Failed to parse TomZonedDateTime string: $formattedString");
  }
  // ...
}

---

Type Safety

1. Use Specific Types

// Good
Map<String, TomObject> _value = {};
List<WeakReference<TomObserver>> _observers = [];
Map<Type, void Function(InstanceMirror, MapEntry<String, dynamic>)> _handlers = {};

// Avoid
var _value = {};
List _observers = [];
Map _handlers = {};

2. Generic Constraints

// Constrain generics appropriately
class TomObject<T> extends TomObservable { ... }
class TomList<E extends TomObject> extends TomObject<List<E>> { ... }
class TomMap<K, V extends TomObject> extends TomObject<Map<K, V>> { ... }
abstract class TomObserver<T extends TomObservable> { ... }

3. Null Safety Patterns

// Optional parameters with defaults
factory TomTimezoned({
  int year = 1970,
  int month = 1,
  TomTimezone? timezone,  // Nullable with null default
}) {
  final tz_ = timezone ?? TomTimezone.utc;  // Resolve null
  // ...
}

// Nullable return for lookup methods
static TomTimezone? fromName(String name) {
  _initialize();
  return _timezonesMap[name.toUpperCase()];  // May return null
}

4. Type Checking Before Operations

void _setComplexValue(..., MapEntry<String, dynamic> entry) {
  if (entry.value is String) {
    _setStringValue(im, imGet, entry);
  } else if (entry.value is Map) {
    _setMapValue(im, cm, imGet, entry);
  } else if (entry.value is List) {
    _setListValue(im, imGet, entry);
  } else {
    throw TomReflectorException(...);
  }
}

---

Memory Management

1. WeakReference for Observers

final List<WeakReference<TomObserver>> _observers = [];

void notifyObservers([dynamic message]) {
  bool hasDeadReferences = false;

  for (var element in _observers) {
    var target = element.target;
    if (target != null) {
      target.onNotify(this);
    } else {
      hasDeadReferences = true;
    }
  }

  // Clean up dead weak references
  if (hasDeadReferences) {
    _observers.removeWhere((ref) => ref.target == null);
  }
}

2. Observer Lifecycle Management

void add(E value) {
  value.addObserver(this);  // Attach on add
  _value.add(value);
  notifyObservers();
}

E removeAt(int index) {
  var result = _value.removeAt(index);
  result.removeObserver(this);  // Detach on remove
  notifyObservers();
  return result;
}

void clear() {
  _detachFrom(_value);  // Detach all before clear
  _value.clear();
  notifyObservers();
}

3. Lazy Initialization

static bool _initialized = false;

static void _initialize() {
  if (_initialized) return;
  _initialized = true;  // Set early to prevent re-entry
  
  tz_data.initializeTimeZones();
  
  for (var entry in tz.timeZoneDatabase.locations.entries) {
    final tomTz = TomTimezone._(
      timezone: entry.value.name,
      location: entry.value,
    );
    _insertTimezone(tomTz);
  }
}

static Map<String, TomTimezone> get timezones {
  _initialize();  // Ensure initialized before use
  return _timezonesMap;
}

---

Testing Considerations

1. Design for Testability

  • Use factory constructors for complex creation logic
  • Provide static is*String() validation methods for parsing
  • Include debug switches for logging during development
  • Make internal state observable when useful

2. Deterministic Values

// Sentinel values for testing
static final TomDateTime noDateTime = TomDateTime(DateTime.utc(1970));
static final TomZonedDate noZonedDate = TomZonedDate.fromDateTime(DateTime(1970));

3. String Format Validation

// Always provide format validation
static bool isTomZonedDateTimeString(String s) {
  return RegExp(
    r"^@X@\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z?_(Z|[+-]\d{2}:\d{2})_[a-zA-Z0-9_/-]+\s?$",
  ).hasMatch(s);
}

---

Complexity Metrics Summary

MetricTargetWarningRefactor
Lines per method≤2025-35>35
Cyclomatic complexity≤78-10>10
Parameters per method≤45-6>6
Nesting depth≤34>4
Class public methods≤1516-20>20
File length (lines)≤500501-800>800

---

Dependency Management

All CLI Tools Must Be Based on tom_build_base

Every CLI tool in the workspace must depend on and be built upon tom_build_base. This ensures consistent CLI behavior across all tools (argument parsing, --version / --help handling, process execution, logging, workspace navigation, etc.).

  • Do NOT implement CLI infrastructure (arg parsing, version flags, help formatting) from scratch in a tool
  • Do use tom_build_base APIs: CliArgParser, isVersionCommand(), isHelpCommand(), ProcessRunner, ToolLogger, etc.
  • If tom_build_base is missing something you need, add it there first, publish a new version, then consume it
  • Tools extending ToolBase (from tom_build_kit) get --version and --help for free; standalone binaries must call these APIs explicitly

NEVER Use Path Overrides to Fix Dependency Errors

When a project fails to compile because a dependency is missing new APIs, the correct fix is ALWAYS to publish the dependency first, then update the version constraint.

Do NOT: - Create pubspec_overrides.yaml with path dependencies - Change pubspec.yaml to use path: ../ instead of version constraints - Replace API calls with inline workarounds (e.g., replacing ProcessRunner.run() with Process.run())

Do: 1. Publish the dependency package to pub.dev (follow the workflow in _copilot_guidelines/dart/project_republishing.md) 2. Update the version constraint in the consuming project's pubspec.yaml 3. Run dart pub upgrade <package> to resolve the new version 4. Verify compilation and tests pass

Step 3 is load-bearing, not a formality: pub get is lock-preserving, so a lower-bound-only constraint admits the new version without ever selecting it. Nested example/ and test/ fixture packages carry their own gitignored locks and need the same treatment. See _copilot_guidelines/dart/project_republishing.md § "Publishing Does Not Reach Existing Consumers by Itself".

Published Package Dependency Rule

All workspace projects that depend on published packages (e.g., tom_build_base, tom_basics, tom_d4rt) must use pub.dev version constraints. Path dependencies are never acceptable in committed code.

See _copilot_guidelines/dart/project_republishing.md for the full publishing workflow and the list of published packages.

---

Refactoring Checklist

When refactoring code, verify:

  • [ ] Methods are under 25 lines
  • [ ] Cyclomatic complexity is ≤ 7
  • [ ] All public classes have doc comments with examples
  • [ ] All public methods have doc comments
  • [ ] Exception keys are hierarchical and unique
  • [ ] Private helpers use _try*, _handle*, _attach* patterns
  • [ ] Observers use WeakReference
  • [ ] Lazy initialization prevents redundant work
  • [ ] Type constraints are as specific as possible
  • [ ] Nullable types are handled explicitly
  • [ ] Section comments organize method groups
  • [ ] File structure follows organization guidelines