Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9b70b6e
Migrate/avoid global state (#227)
Islam-Shaaban-Ibrahim Apr 15, 2026
87f0bd5
migrated avoid non null assertion rule and tests (#230)
Islam-Shaaban-Ibrahim Apr 16, 2026
1119f5c
Migrate/avoid debug print in release mode (#228)
Islam-Shaaban-Ibrahim Apr 16, 2026
04a0112
Migrate/proper super calls (#229)
Islam-Shaaban-Ibrahim Apr 21, 2026
7a8bcfe
Migrated prefer_early_return and its tests (#233)
Dariaa14 Apr 23, 2026
2006202
Migrate avoid unnecessary return variable rule and its tests (#237)
daria-trusca-solid Jun 4, 2026
5bcbbf0
Migrate avoid unnecessary set state rule and its tests (#238)
daria-trusca-solid Jun 4, 2026
adfd585
migrate prefer early return rule with tests (#232)
Islam-Shaaban-Ibrahim Jun 8, 2026
be820c7
Migrate avoid unrelated type assertions and its tests (#239)
daria-trusca-solid Jun 10, 2026
7e8403a
Migrate newline before return rule and its tests (#240)
daria-trusca-solid Jun 10, 2026
4a31bb2
Migrate no equal then else rule and its tests (#241)
daria-trusca-solid Jun 10, 2026
e5eef63
refactor: migrate double_literal_format to analysis_server_plugin (#281)
andrew-bekhiet-solid Jun 10, 2026
1cd1032
Implementation of parameter parser (#234)
Dariaa14 Jun 10, 2026
17d049a
feat: migrate avoid_final_with_getter (#242)
andrew-bekhiet-solid Jun 15, 2026
c54bc50
refactor: migrate avoid_returning_widgets rule (#243)
andrew-bekhiet-solid Jun 15, 2026
13fdf86
refactor: migrate avoid_unnecessary_type_assertions (#282)
andrew-bekhiet-solid Jun 18, 2026
c20a85d
refactor: migrate function_lines_of_code rule (#251)
Jun 18, 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
75 changes: 75 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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_final_with_getter/avoid_final_with_getter_rule.dart';
import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.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';
import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart';
import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart';
import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart';
import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart';
import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart';
import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart';
import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart';
import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart';
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';

/// The entry point for the Solid Lints analyser server plugin.
///
/// This plugin integrates custom lint rules into the Dart analysis server,
/// allowing them to run during static analysis.
final plugin = SolidLintsPlugin();

/// An analysis server plugin that provides Solid lint rules.
///
/// This plugin registers custom lint rules and enables them to be executed
/// by the Dart analyzer during code analysis.
class SolidLintsPlugin extends Plugin {
@override
String get name => 'solid_lints';

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

final avoidUnnecessaryTypeAssertionsRule =
AvoidUnnecessaryTypeAssertionsRule();
final doubleLiteralFormatRule = DoubleLiteralFormatRule();
final lintRules = [
AvoidFinalWithGetterRule(),
AvoidGlobalStateRule(),
AvoidNonNullAssertionRule(),
avoidUnnecessaryTypeAssertionsRule,
AvoidDebugPrintInReleaseRule(),
doubleLiteralFormatRule,
ProperSuperCallsRule(),
AvoidReturningWidgetsRule(
analysisOptionsLoader: analysisLoader,
parametersParser: AvoidReturningWidgetsParameters.fromJson,
),
FunctionLinesOfCodeRule(
analysisOptionsLoader: analysisLoader,
parametersParser: FunctionLinesOfCodeParameters.fromJson,
),
];

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

for (final code in doubleLiteralFormatRule.diagnosticCodes) {
registry.registerFixForRule(code, DoubleLiteralFormatFix.new);
}

registry.registerFixForRule(
AvoidFinalWithGetterRule.code,
AvoidFinalWithGetterFix.new,
);
registry.registerFixForRule(
avoidUnnecessaryTypeAssertionsRule.diagnosticCode,
AvoidUnnecessaryTypeAssertionsFix.new,
);
}
}
77 changes: 0 additions & 77 deletions lib/solid_lints.dart

This file was deleted.

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;
}
Comment on lines +61 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The loop condition while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) will terminate immediately if packageRootPath is the root directory (e.g., / or C:\), meaning it will never check for analysis_options.yaml at the root directory itself.

To make this search robust across all environments (including containers or environments where the project is at the root), we should check the current directory first before checking the loop termination condition.

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

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

      if (candidateFile.exists) {
        return candidatePath;
      }

      final parentDir = pathContext.dirname(currentDirectoryPath);
      if (parentDir == currentDirectoryPath) {
        break;
      }
      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 @@ -82,7 +82,7 @@ class ExcludedIdentifiersListParameter {
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>();

return classDeclaration != null &&
classDeclaration.name.toString() == className;
classDeclaration.namePart.typeName.lexeme == className;
}
}
}
Loading
Loading