Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6275b4c
Created analysis_options.yaml rules parser
Dariaa14 Apr 22, 2026
48860fb
Improved yaml parser and added the analysis_options loader
Dariaa14 Apr 23, 2026
186220b
Improved rules loader from yaml
Dariaa14 Apr 23, 2026
67fb836
Added verification before looking for .yaml's path
Dariaa14 Apr 23, 2026
c770d17
Fields and getters are now declared before the constructor
Dariaa14 Apr 23, 2026
4f7b35e
Added method to get options of a rule by it's name
Dariaa14 Apr 23, 2026
a8a3643
Made suggested changes to file upward finder
Dariaa14 Apr 23, 2026
8b0e904
Removed top-level variable
Dariaa14 Apr 23, 2026
a89dc96
Improved name of variable in loadRuleFromContext
Dariaa14 Apr 23, 2026
1da8d7e
Updated analysis options to have rules for each configuration file path
Dariaa14 Apr 23, 2026
ae8520a
Updated file upward finder to not mix File from dart.io with file fro…
Dariaa14 Apr 23, 2026
a6e227c
Added usage example in avoid_global_state_rule
Dariaa14 Apr 23, 2026
48063bb
style: move getters and fields before constructor
andrew-bekhiet-solid Jun 2, 2026
e92ba3e
style: improve readability
andrew-bekhiet-solid Jun 2, 2026
da89ff9
fix: don't parse enabled if the rule has configured options
andrew-bekhiet-solid Jun 2, 2026
5ca9f16
feat: reload rules from file if newer
andrew-bekhiet-solid Jun 2, 2026
73d09a5
test: add AnalysisOptionsLoaderTest
andrew-bekhiet-solid Jun 2, 2026
ca08701
feat(SolidLintRule): add parameter parsing
andrew-bekhiet-solid Jun 3, 2026
387f6f9
fix: use Map<String, Object?> for raw rule config
andrew-bekhiet-solid Jun 3, 2026
16b22b6
fix: method name
andrew-bekhiet-solid Jun 3, 2026
a122898
fix: make sure rules options are loaded before getting parameters
andrew-bekhiet-solid Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:analysis_server_plugin/plugin.dart';
import 'package:analysis_server_plugin/registry.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart';
import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart';
import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
Expand All @@ -21,8 +22,9 @@ class SolidLintsPlugin extends Plugin {

@override
void register(PluginRegistry registry) {
final analysisLoader = AnalysisOptionsLoader();
registry.registerLintRule(
AvoidGlobalStateRule(),
AvoidGlobalStateRule(analysisLoader),
);
registry.registerLintRule(
AvoidNonNullAssertionRule(),
Expand Down
104 changes: 104 additions & 0 deletions lib/src/common/parameter_parser/analysis_options_loader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:solid_lints/src/common/parameter_parser/cached_package_rules.dart';
import 'package:yaml/yaml.dart';

/// Loads and parses analysis options from a Dart project's YAML file.
class AnalysisOptionsLoader {
final ResourceProvider _resourceProvider;
final Map<String, CachedPackageRules> _rulesCache = {};

/// Creates an instance of [AnalysisOptionsLoader]
AnalysisOptionsLoader({ResourceProvider? resourceProvider})
: _resourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE;

/// Gets the options for a specific rule by its name.
Map<String, Object?>? getRuleOptions(RuleContext context, String ruleName) {
final packageRootPath = context.package?.root.path;
if (packageRootPath == null) return null;

final yamlPath = _findNearestAnalysisOptionsFilePath(packageRootPath);
if (yamlPath == null) return null;

return _rulesCache[yamlPath]?.rules[ruleName];
}

/// Loads lint rules from the analysis options file for all rules
/// using the provided [RuleContext].
void loadRulesOptionsFromContext(RuleContext context) {
final packageRootPath = context.package?.root.path;
if (packageRootPath == null) return;

_loadRulesOptionsIfNewer(packageRootPath);
}

void _loadRulesOptionsIfNewer(String rootPath) {
final yamlPath = _findNearestAnalysisOptionsFilePath(rootPath);
if (yamlPath == null) return;

final analysisOptionsFile = _resourceProvider.getFile(yamlPath);
final modificationStamp = analysisOptionsFile.modificationStamp;
final cachedRules = _rulesCache[yamlPath];

if (cachedRules?.modificationStamp == modificationStamp) {
return;
}

final rules = _getRules(analysisOptionsFile);
_rulesCache[yamlPath] = CachedPackageRules(
modificationStamp: modificationStamp,
rules: rules,
);
}

String? _findNearestAnalysisOptionsFilePath(String packageRootPath) {
final pathContext = _resourceProvider.pathContext;
String currentDirectoryPath = packageRootPath;

while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) {
final candidatePath =
pathContext.join(currentDirectoryPath, 'analysis_options.yaml');
final candidateFile = _resourceProvider.getFile(candidatePath);

if (candidateFile.exists) {
return candidatePath;
}

final parentDir = pathContext.dirname(currentDirectoryPath);
currentDirectoryPath = parentDir;
}

return null;
}

Map<String, Map<String, Object?>> _getRules(File? analysisOptionsFile) {
if (analysisOptionsFile == null || !analysisOptionsFile.exists) {
return {};
}

final optionsString = analysisOptionsFile.readAsStringSync();
Object? yaml;
try {
yaml = loadYaml(optionsString) as Object?;
} catch (err) {
return {};
}

if (yaml
case {'plugins': {'solid_lints': {'diagnostics': final diagnostics?}}}
when diagnostics is Map) {
return Map.fromEntries(
diagnostics.entries.where((e) => e.key is String && e.value is Map).map(
(e) => MapEntry(
e.key as String,
Map<String, Object?>.from(e.value as Map),
),
),
);
}

return {};
}
}
14 changes: 14 additions & 0 deletions lib/src/common/parameter_parser/cached_package_rules.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Cached rules for a dart package
class CachedPackageRules {
/// The last modification stamp of the analysis options file
final int modificationStamp;

/// Cached rules options by rule name for the package
final Map<String, Map<String, Object?>> rules;

/// Creates an instance of [CachedPackageRules]
const CachedPackageRules({
required this.modificationStamp,
required this.rules,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/error/error.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
import 'package:solid_lints/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart';

/// Avoid top-level and static mutable variables.
Expand Down Expand Up @@ -46,8 +47,10 @@ class AvoidGlobalStateRule extends AnalysisRule {
'Prefer using final/const or a state management solution.',
);

final AnalysisOptionsLoader _analysisLoader;

/// Creates an instance of [AvoidGlobalStateRule].
AvoidGlobalStateRule()
AvoidGlobalStateRule(this._analysisLoader)
: super(
name: lintName,
description: 'Avoid top-level or static mutable variables ',
Expand All @@ -63,6 +66,10 @@ class AvoidGlobalStateRule extends AnalysisRule {
) {
final visitor = AvoidGlobalStateVisitor(this);

_analysisLoader.loadRulesOptionsFromContext(context);
// To get the options of the rule:
// _analysisLoader.getRuleOptions(context, lintName);

registry.addTopLevelVariableDeclaration(this, visitor);
registry.addFieldDeclaration(this, visitor);
}
Expand Down
43 changes: 0 additions & 43 deletions lib/src/models/rule_config.dart

This file was deleted.

45 changes: 36 additions & 9 deletions lib/src/models/solid_lint_rule.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';

/// A function that parses the rule parameters from analysis options json
typedef RuleParametersParser<T> = T Function(Map<String, Object?>);

/// A base class for emitting information about
/// issues with user's `.dart` files.
abstract class SolidLintRule<T extends Object?> extends DartLintRule {
abstract class SolidLintRule<T extends Object?> extends AnalysisRule {
final AnalysisOptionsLoader? _analysisOptionsLoader;

final RuleParametersParser<T>? _parametersParser;

/// Constructor for [SolidLintRule] model.
SolidLintRule(this.config) : super(code: config.lintCode);
SolidLintRule({
required super.name,
required super.description,
super.state,
}) : _analysisOptionsLoader = null,
_parametersParser = null;

/// Constructor for [SolidLintRule] model with parameters.
SolidLintRule.withParameters({
required AnalysisOptionsLoader analysisOptionsLoader,
required RuleParametersParser<T> parametersParser,
required super.name,
required super.description,
super.state,
}) : _analysisOptionsLoader = analysisOptionsLoader,
_parametersParser = parametersParser;

/// Reads the rule parameters from analysis options and parses them to [T]
T? getParametersForContext(RuleContext context) {
_analysisOptionsLoader?.loadRulesOptionsFromContext(context);

/// Configuration for a particular rule with all the
/// defined custom parameters.
final RuleConfig<T> config;
final unparsedParameters =
_analysisOptionsLoader?.getRuleOptions(context, name);
if (unparsedParameters == null) return null;

/// A flag which indicates whether this rule was enabled by the user.
bool get enabled => config.enabled;
return _parametersParser?.call(unparsedParameters);
}
}
Loading
Loading