Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
43a6648
Created analysis_options.yaml rules parser
Dariaa14 Apr 22, 2026
87abe51
Improved yaml parser and added the analysis_options loader
Dariaa14 Apr 23, 2026
4c604a3
Improved rules loader from yaml
Dariaa14 Apr 23, 2026
87f5870
Added verification before looking for .yaml's path
Dariaa14 Apr 23, 2026
ca9df7c
Fields and getters are now declared before the constructor
Dariaa14 Apr 23, 2026
226a748
Added method to get options of a rule by it's name
Dariaa14 Apr 23, 2026
08c3e8e
Made suggested changes to file upward finder
Dariaa14 Apr 23, 2026
1eee0b2
Removed top-level variable
Dariaa14 Apr 23, 2026
0ef7917
Improved name of variable in loadRuleFromContext
Dariaa14 Apr 23, 2026
86c3f4d
Updated analysis options to have rules for each configuration file path
Dariaa14 Apr 23, 2026
e0490f7
Updated file upward finder to not mix File from dart.io with file fro…
Dariaa14 Apr 23, 2026
03a53ba
Added usage example in avoid_global_state_rule
Dariaa14 Apr 23, 2026
9f90265
style: move getters and fields before constructor
andrew-bekhiet-solid Jun 2, 2026
a8d53e4
style: improve readability
andrew-bekhiet-solid Jun 2, 2026
73514fd
fix: don't parse enabled if the rule has configured options
andrew-bekhiet-solid Jun 2, 2026
c6a2453
feat: reload rules from file if newer
andrew-bekhiet-solid Jun 2, 2026
55af03a
test: add AnalysisOptionsLoaderTest
andrew-bekhiet-solid Jun 2, 2026
06c5367
feat(SolidLintRule): add parameter parsing
andrew-bekhiet-solid Jun 3, 2026
affc62c
fix: use Map<String, Object?> for raw rule config
andrew-bekhiet-solid Jun 3, 2026
1b10b3d
fix: method name
andrew-bekhiet-solid Jun 3, 2026
4c30ac9
fix: make sure rules options are loaded before getting parameters
andrew-bekhiet-solid Jun 3, 2026
1cb4497
refactor: remove unused AnalysisOptionsLoader from AvoidGlobalStateRule
andrew-bekhiet-solid Jun 8, 2026
0975ba7
refactor: extract duplicate logic
andrew-bekhiet-solid Jun 8, 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
24 changes: 14 additions & 10 deletions 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 @@ -23,21 +24,24 @@ class SolidLintsPlugin extends Plugin {

@override
void register(PluginRegistry registry) {
registry.registerLintRule(
final analysisLoader = AnalysisOptionsLoader();

final doubleLiteralFormatRule = DoubleLiteralFormatRule();
final lintRules = [
AvoidGlobalStateRule(),
);
registry.registerLintRule(
AvoidNonNullAssertionRule(),
);
registry.registerLintRule(
AvoidDebugPrintInReleaseRule(),
);
registry.registerLintRule(
doubleLiteralFormatRule,
ProperSuperCallsRule(),
);
// TODO: Add more lint rules and use analysisLoader
// for rules that need parameters
// For example: `CyclomaticComplexityRule(analysisLoader)`
];

for (final lintRule in lintRules) {
registry.registerLintRule(lintRule);
}

final doubleLiteralFormatRule = DoubleLiteralFormatRule();
registry.registerLintRule(doubleLiteralFormatRule);
for (final code in doubleLiteralFormatRule.diagnosticCodes) {
registry.registerFixForRule(code, DoubleLiteralFormatFix.new);
}
Expand Down
109 changes: 109 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,109 @@
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) =>
_withNearestAnalysisOptionsFilePathForContext<Map<String, Object?>?>(
context,
(path) => _rulesCache[path]?.rules[ruleName],
);

/// Loads lint rules from the analysis options file for all rules
/// using the provided [RuleContext].
void loadRulesOptionsFromContext(RuleContext context) =>
_withNearestAnalysisOptionsFilePathForContext(
context,
_loadRulesOptionsIfNewer,
);

T? _withNearestAnalysisOptionsFilePathForContext<T>(
RuleContext context,
T Function(String) f,
) {
final packageRootPath = context.package?.root.path;
if (packageRootPath == null) return null;

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

return f(yamlPath);
}

void _loadRulesOptionsIfNewer(String yamlPath) {
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,
});
}
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