Skip to content
8 changes: 8 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import 'package:solid_lints/src/lints/double_literal_format/double_literal_forma
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/member_ordering/member_ordering_rule.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart';
import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart';
import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart';
import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart';
Expand Down Expand Up @@ -58,6 +60,7 @@ class SolidLintsPlugin extends Plugin {
AvoidDebugPrintInReleaseRule(),
doubleLiteralFormatRule,
ProperSuperCallsRule(),
NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader),
AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader),
MemberOrderingRule(analysisOptionsLoader: analysisLoader),
AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader),
Expand Down Expand Up @@ -106,5 +109,10 @@ class SolidLintsPlugin extends Plugin {
preferLastRule.diagnosticCode,
PreferLastFix.new,
);

registry.registerFixForRule(
NamedParametersOrderingRule.code,
NamedParametersOrderingFix.new,
);
}
}
16 changes: 16 additions & 0 deletions lib/src/common/parameter_parser/analysis_options_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ class AnalysisOptionsLoader {
(path) => _rulesCache[path]?.rules[ruleName],
);

/// Gets the options for a specific rule by looking up the nearest
/// `analysis_options.yaml` from the given [filePath]'s directory.
///
/// Unlike [getRuleOptions], this method does not require a [RuleContext]
/// and can be used from quick fixes.
Map<String, Object?>? getRuleOptionsForFile(
String filePath,
String ruleName,
) {
final dirPath = _resourceProvider.pathContext.dirname(filePath);
final yamlPath = _findNearestAnalysisOptionsFilePath(dirPath);
if (yamlPath == null) return null;
_loadRulesOptionsIfNewer(yamlPath);
return _rulesCache[yamlPath]?.rules[ruleName];
}

/// Loads lint rules from the analysis options file for all rules
/// using the provided [RuleContext].
void loadRulesOptionsFromContext(RuleContext context) =>
Expand Down
27 changes: 14 additions & 13 deletions lib/src/lints/named_parameters_ordering/config_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,23 @@

import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart';

/// Helper class to parse member_ordering rule config
/// Helper class to parse named_parameters_ordering rule config
class NamedParametersConfigParser {
static const _defaultOrderList = [
'required_super',
'super',
'required',
'nullable',
'default',
];

/// Parse rule config for regular class order rules
static List<ParameterType> parseOrder(Object? orderConfig) {
final order = orderConfig is Iterable
? List<String>.from(orderConfig)
: _defaultOrderList;
if (orderConfig is! Iterable) {
return ParameterType.defaultOrder;
}

return order.map(ParameterType.fromType).nonNulls.toList();
final parsed = orderConfig
.whereType<String>()
.map(ParameterType.fromType)
.nonNulls
.toSet()
.toList();
Comment thread
solid-illiaaihistov marked this conversation as resolved.
final missing = ParameterType.defaultOrder.where(
(type) => !parsed.contains(type),
);
return [...parsed, ...missing];
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:collection/collection.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart';
import 'package:solid_lints/src/utils/correction_utils.dart';

/// A parameter block: the offsets of a parameter (including leading comments
/// and indentation) and an optional trailing comment on the same line.
typedef _ParamBlock = ({int blockStart, int blockEnd, String? trailingComment});

/// A Quick fix for [NamedParametersOrderingRule] rule.
class NamedParametersOrderingFix extends ResolvedCorrectionProducer {
static const _fixKind = FixKind(
'solid_lints.fix.named_parameters_ordering',
DartFixKindPriority.standard,
"Sort named parameters",
);

/// Creates a new instance of [NamedParametersOrderingFix].
NamedParametersOrderingFix({required super.context});

@override
FixKind get fixKind => _fixKind;

@override
CorrectionApplicability get applicability =>
CorrectionApplicability.automatically;

@override
Future<void> compute(ChangeBuilder builder) async {
final parameterList = node.thisOrAncestorOfType<FormalParameterList>();
if (parameterList == null) return;

final namedParams = parameterList.parameters
.where((p) => p.isNamed)
.toList();
if (namedParams.length < 2) return;

final parametersOrder = _getParametersOrder();

final sortedNamedParams = namedParams.sortedBy(
(e) => parametersOrder.indexOf(ParameterType.fromParameter(e)),
);

// Check if the order is already correct (if sorting changed nothing)
final isChanged = !const ListEquality<FormalParameter>().equals(
namedParams,
sortedNamedParams,
);
if (!isChanged) return;

final isMultiline = utils
.getRangeText(parameterList.sourceRange)
.contains('\n');

final hasComments = namedParams.any(
(p) => p.beginToken.precedingComments != null,
);

final sourceStart = namedParams.first.offset;
final sourceEnd = namedParams.last.end;

if (!isMultiline && !hasComments) {
// Single-line: no leading comments, simple text replacement
return builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(
utils.createRange(sourceStart, sourceEnd),
sortedNamedParams
.map((p) => utils.getRangeText(p.sourceRange))
.join(', '),
);
});
}

// Multiline: extract parameter blocks including leading and trailing
// comments
final blocks = _extractParamBlocks(
namedParams,
parameterList,
);

// Map sorted parameters to their corresponding blocks
final sortedBlocks = sortedNamedParams
.map((p) => blocks[namedParams.indexOf(p)])
.toList();

// Determine if original had a trailing comma after the last param
final tokenAfterEnd = namedParams.last.endToken.next;
final hasTrailingComma = tokenAfterEnd?.lexeme == ',';

// Build replacement text preserving trailing comments
final replacement = sortedBlocks.expandIndexed((i, e) {
final isLast = i == sortedBlocks.length - 1;
final trailingComment = e.trailingComment;
final text = utils.getTextRange(e.blockStart, e.blockEnd);
return [
text,
if (!isLast || hasTrailingComma) ',',
if (trailingComment != null) ' $trailingComment',
if (!isLast) '\n',
];
}).join();

// Extend range to include the original trailing comma and any trailing
// comment on the original last parameter's line.
var rangeEnd = hasTrailingComma ? tokenAfterEnd!.end : sourceEnd;
final upperBound =
parameterList.rightDelimiter?.offset ??
parameterList.rightParenthesis.offset;
if (rangeEnd < upperBound) {
final afterLast = utils.getTextRange(rangeEnd, upperBound);
final newlineIdx = afterLast.indexOf('\n');
if (newlineIdx != -1) {
rangeEnd += newlineIdx;
}
}

final firstBlockStart = blocks.first.blockStart;
final targetRange = utils.createRange(firstBlockStart, rangeEnd);

await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(targetRange, replacement);
});
}

/// Extracts text blocks for each named parameter, including any leading
/// comments that belong to that parameter, and detects trailing comments
/// on the same line.
///
/// Trailing comments (e.g., `// comment` after a parameter on the same line)
/// are attributed to the parameter they follow, not the next parameter.
List<_ParamBlock> _extractParamBlocks(
List<FormalParameter> namedParams,
FormalParameterList parameterList,
) {
final blocks = <_ParamBlock>[];

final lowerBound =
parameterList.leftDelimiter?.end ?? parameterList.leftParenthesis.end;
final upperBound =
parameterList.rightDelimiter?.offset ??
parameterList.rightParenthesis.offset;

for (int i = 0; i < namedParams.length; i++) {
final param = namedParams[i];
final int minOffset = i == 0 ? lowerBound : namedParams[i - 1].end;

final nextParamStart = i < namedParams.length - 1
? namedParams[i + 1].offset
: upperBound;
final previousParamEnd = i > 0 ? namedParams[i - 1].end : null;

final leadingComment = utils.getLeadingComment(
node: param,
previousEnd: previousParamEnd,
);
final blockStart = utils.getDeclarationStartOffset(
node: param,
leadingComment: leadingComment,
minOffset: minOffset,
);
final trailingComment = utils.getTrailingComment(
node: param,
nextOffset: nextParamStart,
);

blocks.add((
blockStart: blockStart,
blockEnd: param.end,
trailingComment: trailingComment,
));
}

return blocks;
}

List<ParameterType> _getParametersOrder() {
final loader = AnalysisOptionsLoader(
resourceProvider: resourceProvider,
);
final options = loader.getRuleOptionsForFile(
file,
NamedParametersOrderingRule.lintName,
);
return options == null
? ParameterType.defaultOrder
: NamedParametersOrderingParameters.fromJson(options).order;
}
}
43 changes: 43 additions & 0 deletions lib/src/lints/named_parameters_ordering/models/parameter_type.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:collection/collection.dart';

/// Represents a function parameter type
Expand All @@ -17,11 +18,53 @@ enum ParameterType {
/// Default value parameter type (String parameterName = 'defaultValue')
defaultValue('default');

/// The default ordering of parameter types.
static const defaultOrder = [
ParameterType.requiredInherited,
ParameterType.inherited,
ParameterType.required,
ParameterType.nullable,
ParameterType.defaultValue,
];

/// Returns [ParameterType] from type or null if not found
static ParameterType? fromType(String type) {
return values.firstWhereOrNull((o) => o.type == type);
}

/// Classifies a [FormalParameter] into a [ParameterType].
///
/// Recursively unwraps [DefaultFormalParameter] wrappers to determine
/// the underlying parameter kind.
static ParameterType fromParameter(
FormalParameter parameter, {
bool hasDefaultValue = false,
}) {
if (parameter is DefaultFormalParameter &&
parameter.parameter is! DefaultFormalParameter) {
return fromParameter(
parameter.parameter,
hasDefaultValue: parameter.defaultValue != null,
);
}

switch (parameter) {
case SuperFormalParameter(:final isRequired):
return isRequired
? ParameterType.requiredInherited
: ParameterType.inherited;

case DefaultFormalParameter():
case _ when hasDefaultValue:
return ParameterType.defaultValue;
Comment thread
solid-illiaaihistov marked this conversation as resolved.

case FieldFormalParameter(:final isRequired) ||
FunctionTypedFormalParameter(:final isRequired) ||
SimpleFormalParameter(:final isRequired):
return isRequired ? ParameterType.required : ParameterType.nullable;
}
}

/// String representation of the parameter type
final String type;

Expand Down
Loading
Loading