Skip to content

feat: support configurable rule-specific options and custom exclusions in path-does-not-exist#176

Open
reidbaker-agent wants to merge 10 commits into
flutter:mainfrom
reidbaker:feature/rule-specific-config
Open

feat: support configurable rule-specific options and custom exclusions in path-does-not-exist#176
reidbaker-agent wants to merge 10 commits into
flutter:mainfrom
reidbaker:feature/rule-specific-config

Conversation

@reidbaker-agent

@reidbaker-agent reidbaker-agent commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

feat: Support Configurable Rule-Specific Options and Custom Exclusions

Context & Goal

Previously, rule evaluations were static and could not accept custom runtime configurations or parameters from the configuration file (dart_skills_lint.yaml) or command-line overrides.

This PR introduces the concept of rule-specific options/parameters to make rules configurable. We then leverage this feature to add a custom exclude option to path-does-not-exist, allowing users to skip validation on matching skill folders (e.g. workspace or integration test subdirectories).


Architectural Design & Key Features

  1. Configurable Rule-Specific Options:
    • Introduced CustomRuleOptions as a type-safe wrapper for rule-specific parameters, replacing loose maps throughout the validation pipeline.
    • Introduced ConfigurableSkillRule as the base class for any rule that accepts custom options.
  2. Exclusions on path-does-not-exist:
    • Refactored PathDoesNotExistRule to extend ConfigurableSkillRule.
    • It now accepts an exclude option (regular expression pattern string) to skip directories that match the pattern.
  3. Options Schema Validation & Merging:
    • Consolidated key and value type verification into CheckType.validateOptions.
    • Options schemas are defined exactly once under RuleRegistry.allChecks.
    • Validation checks are automatically run at both configuration-parsing time (returning formatting messages) and programmatically during constructor instantiation (throwing ArgumentError).
    • ValidationSession resolves target-specific, global, and CLI flag overrides into the final options map.

- Rename Validator parameter ruleOverrides to customRuleSeverities.
- Rename severity mapping fields and parameters to ruleSeverities.
- Rename option mapping fields and parameters to ruleOptions.
- Added validation checks for options keys and types in YAML parsing.
- Decoupled and exported ValidationResult model.
… wrapper

- Extract options validation to ConfigurableSkillRule base constructor.
- Introduce CustomRuleOptions class wrapping options configuration maps.
- Refactor PathDoesNotExistRule to extend ConfigurableSkillRule.
…tions

- Centralize options schema validation and formatting in CheckType.validateOptions.
- Remove redundant allowedOptions getter from ConfigurableSkillRule and its subclasses.
- Update ConfigParser, entrypoint, Validator, and ValidationSession to use CustomRuleOptions instead of raw maps.
- Add unit tests for resolving options and testing PathDoesNotExistRule with custom exclusions.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for rule-specific custom options in dart_skills_lint.yaml and refactors the path-does-not-exist check into a class-based, configurable SkillRule supporting an exclude option. The feedback highlights critical improvements, including preventing rules from being silently disabled when the severity key is omitted in map-based configurations, avoiding calling overridable getters inside the ConfigurableSkillRule constructor, and validating regular expression patterns during options validation to prevent unhandled crashes.

Comment on lines +146 to +148
if (value is YamlMap) {
final severityStr = value[_severityKey]?.toString() ?? '';
rules[ruleName] = _parseSeverity(severityStr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When parsing map-based rule configurations, if the severity key is omitted, the rule's severity is currently set to disabled (since _parseSeverity('') defaults to disabled). This silently disables the rule when a user only wants to configure custom options. We should only override the severity if the severity key is explicitly present in the map.

Suggested change
if (value is YamlMap) {
final severityStr = value[_severityKey]?.toString() ?? '';
rules[ruleName] = _parseSeverity(severityStr);
if (value is YamlMap) {
if (value.containsKey(_severityKey)) {
final severityStr = value[_severityKey]?.toString() ?? '';
rules[ruleName] = _parseSeverity(severityStr);
}

Comment on lines +15 to +38
abstract class ConfigurableSkillRule extends SkillRule {
ConfigurableSkillRule(this.customRuleOptions) {
_validateOptions();
}

/// The parsed custom configuration options for this rule.
final CustomRuleOptions? customRuleOptions;

void _validateOptions() {
final CustomRuleOptions? opts = customRuleOptions;
if (opts == null) {
return;
}
// Rules must have a unique name so we can assume one match.
final Iterable<CheckType> checkMatches = RuleRegistry.allChecks.where((c) => c.name == name);
if (checkMatches.isEmpty) {
return;
}
final CheckType check = checkMatches.first;
final List<String> errors = check.validateOptions(opts);
if (errors.isNotEmpty) {
throw ArgumentError(errors.join('\n'));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling overridable members (like the name getter) inside a constructor is an anti-pattern in Dart because the subclass is not yet fully initialized. If a subclass overrides name as a field, it will be null during super constructor execution. We should pass the rule name explicitly to the ConfigurableSkillRule constructor to ensure safe initialization.

abstract class ConfigurableSkillRule extends SkillRule {
  ConfigurableSkillRule(this.customRuleOptions, String ruleName) {
    _validateOptions(ruleName);
  }

  /// The parsed custom configuration options for this rule.
  final CustomRuleOptions? customRuleOptions;

  void _validateOptions(String ruleName) {
    final CustomRuleOptions? opts = customRuleOptions;
    if (opts == null) {
      return;
    }
    // Rules must have a unique name so we can assume one match.
    final Iterable<CheckType> checkMatches = RuleRegistry.allChecks.where((c) => c.name == ruleName);
    if (checkMatches.isEmpty) {
      return;
    }
    final CheckType check = checkMatches.first;
    final List<String> errors = check.validateOptions(opts);
    if (errors.isNotEmpty) {
      throw ArgumentError(errors.join('\n'));
    }
  }

Comment on lines +19 to +21
class PathDoesNotExistRule extends ConfigurableSkillRule {
PathDoesNotExistRule({required this.severity, CustomRuleOptions? customRuleOptions})
: super(customRuleOptions) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass the ruleName static constant to the super constructor to avoid calling the overridable name getter during object construction.

Suggested change
class PathDoesNotExistRule extends ConfigurableSkillRule {
PathDoesNotExistRule({required this.severity, CustomRuleOptions? customRuleOptions})
: super(customRuleOptions) {
class PathDoesNotExistRule extends ConfigurableSkillRule {
PathDoesNotExistRule({required this.severity, CustomRuleOptions? customRuleOptions})
: super(customRuleOptions, ruleName) {

Comment on lines +36 to +41
}
final Type expectedType = allowedOptions[key]!;
final Object? actualValue = options[key];
if (actualValue != null && !_isTypeValid(actualValue, expectedType)) {
errors.add(
'Invalid type for option "$key" in rule "$name". '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

An invalid regular expression pattern in the exclude option of path-does-not-exist will cause the linter to crash with an unhandled FormatException during rule construction. We should validate that the pattern is a valid regular expression during options validation so that configuration errors are reported cleanly to the user.

      if (actualValue != null && !_isTypeValid(actualValue, expectedType)) {
        errors.add(
          'Invalid type for option "$key" in rule "$name". '
          'Expected $expectedType, got "$actualValue" (${actualValue.runtimeType}).',
        );
      } else if (actualValue is String && name == 'path-does-not-exist' && key == 'exclude') {
        try {
          RegExp(actualValue);
        } on FormatException catch (e) {
          errors.add(
            'Invalid regular expression for option "$key" in rule "$name": ${e.message}',
          );
        }
      }

@reidbaker-agent reidbaker-agent changed the title refactor: introduce ConfigurableSkillRule and CustomRuleOptions feat: support configurable rule-specific options and custom exclusions in path-does-not-exist Jul 14, 2026
reidbaker and others added 5 commits July 13, 2026 22:15
… to optionsSchema

- Add type-safe getters (getString, getInt, getBool, getStringList) to CustomRuleOptions.
- Update PathDoesNotExistRule to retrieve options using the new type-safe getters.
- Rename allowedOptions to optionsSchema inside CheckType to distinguish the schema definition from configuration values.
- Clean up all references across EntryPoint, Registry, and tests to match optionsSchema.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants