Tom Coding Guidelines
Guidelines derived from the reflection, observable, and timezoned
modules for consistent, maintainable code.
Table of Contents
- Code Structure
- Method Design
- Documentation Standards
- Naming Conventions
- Error Handling
- Type Safety
- Memory Management
- Testing Considerations
- Dependency Management
---
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 Type | Target Lines | Max Lines |
|---|---|---|
| Public API methods | 10-20 | 30 |
| Private helpers | 5-15 | 20 |
| Factory constructors | 10-15 | 20 |
| Simple getters/setters | 1-3 | 5 |
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
| Type | Pattern | Examples |
|---|---|---|
| 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 classes | Concept + Info | TomReflectionInfo |
2. Method Names
| Pattern | Purpose | Examples |
|---|---|---|
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 string | parseFromString() |
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=valueformat (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
| Metric | Target | Warning | Refactor |
|---|---|---|---|
| Lines per method | ≤20 | 25-35 | >35 |
| Cyclomatic complexity | ≤7 | 8-10 | >10 |
| Parameters per method | ≤4 | 5-6 | >6 |
| Nesting depth | ≤3 | 4 | >4 |
| Class public methods | ≤15 | 16-20 | >20 |
| File length (lines) | ≤500 | 501-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_baseAPIs:CliArgParser,isVersionCommand(),isHelpCommand(),ProcessRunner,ToolLogger, etc. -
If
tom_build_baseis missing something you need, add it there first, publish a new version, then consume it -
Tools extending
ToolBase(fromtom_build_kit) get--versionand--helpfor 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