Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
fd624c4
Migrate avoid_late_keyword rule (default behavior) and tests
Islam-Shaaban-Ibrahim Apr 16, 2026
6092317
feat: read parameter by extending SolidLintRule
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
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,
});
}
80 changes: 27 additions & 53 deletions lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.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/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';
import 'package:solid_lints/src/utils/types_utils.dart';

/// Avoid `late` keyword
///
Expand Down Expand Up @@ -48,62 +47,37 @@ import 'package:solid_lints/src/utils/types_utils.dart';
/// }
/// ```
class AvoidLateKeywordRule extends SolidLintRule<AvoidLateKeywordParameters> {
/// This lint rule represents
/// the error whether we use `late` keyword.
static const lintName = 'avoid_late_keyword';
static const String _lintName = 'avoid_late_keyword';

AvoidLateKeywordRule._(super.config);
static const LintCode _code = LintCode(
_lintName,
'Avoid using the "late" keyword. It may result in runtime exceptions.',
);

/// Creates a new instance of [AvoidLateKeywordRule]
/// based on the lint configuration.
factory AvoidLateKeywordRule.createRule(CustomLintConfigs configs) {
final rule = RuleConfig(
configs: configs,
name: lintName,
paramsParser: AvoidLateKeywordParameters.fromJson,
problemMessage: (_) => 'Avoid using the "late" keyword. '
'It may result in runtime exceptions.',
);
/// Creates an instance of [AvoidLateKeywordRule].
AvoidLateKeywordRule({required super.analysisOptionsLoader})
: super.withParameters(
name: _lintName,
description: 'Warns against using the late keyword.',
parametersParser: AvoidLateKeywordParameters.fromJson,
);

return AvoidLateKeywordRule._(rule);
}
@override
LintCode get diagnosticCode => _code;

@override
void run(
CustomLintResolver resolver,
DiagnosticReporter reporter,
CustomLintContext context,
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
context.registry.addVariableDeclaration((node) {
if (_shouldLint(node)) {
reporter.atNode(node, code);
}
});
}

bool _shouldLint(VariableDeclaration node) {
final isLateDeclaration = node.isLate;
if (!isLateDeclaration) return false;

final hasIgnoredType = _hasIgnoredType(node);
if (hasIgnoredType) return false;

final allowInitialized = config.parameters.allowInitialized;
if (!allowInitialized) return true;

final hasInitializer = node.initializer != null;
return !hasInitializer;
}

bool _hasIgnoredType(VariableDeclaration node) {
final ignoredTypes = config.parameters.ignoredTypes.toSet();
if (ignoredTypes.isEmpty) return false;
final parameters =
getParametersForContext(context) ?? const AvoidLateKeywordParameters();

final variableType = node.declaredFragment?.element.type;
if (variableType == null) return false;
final visitor = AvoidLateKeywordVisitor(this, parameters);

return variableType.hasIgnoredType(
ignoredTypes: ignoredTypes,
registry.addVariableDeclaration(
this,
visitor,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart';
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.
import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart';
import 'package:solid_lints/src/utils/types_utils.dart';

/// Visitor for [AvoidLateKeywordRule].
class AvoidLateKeywordVisitor extends SimpleAstVisitor<void> {
final AvoidLateKeywordRule _rule;

final AvoidLateKeywordParameters _parameters;

/// Creates an instance of [AvoidLateKeywordVisitor].
AvoidLateKeywordVisitor(this._rule, this._parameters);

@override
void visitVariableDeclaration(VariableDeclaration node) {
if (!_shouldReport(node)) return;

_rule.reportAtNode(node);
}

bool _shouldReport(VariableDeclaration node) {
final isLateDeclaration = node.isLate;
if (!isLateDeclaration) return false;

final hasIgnoredType = _hasIgnoredType(node);
if (hasIgnoredType) return false;

final allowInitialized = _parameters.allowInitialized;
if (!allowInitialized) return true;

final hasInitializer = node.initializer != null;
return !hasInitializer;
}

bool _hasIgnoredType(VariableDeclaration node) {
final ignoredTypes = _parameters.ignoredTypes.toSet();
if (ignoredTypes.isEmpty) return false;

final variableType = node.declaredFragment?.element.type;
if (variableType == null) return false;

return variableType.hasIgnoredType(
ignoredTypes: ignoredTypes,
);
}
}
43 changes: 0 additions & 43 deletions lib/src/models/rule_config.dart

This file was deleted.

Loading
Loading