Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/av
import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.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/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/proper_super_calls/proper_super_calls_rule.dart';
import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart';
import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart';
Expand Down Expand Up @@ -53,6 +55,9 @@ class SolidLintsPlugin extends Plugin {
AvoidUnusedParametersRule(
analysisOptionsLoader: analysisLoader,
),
NamedParametersOrderingRule(
analysisOptionsLoader: analysisLoader,
),
UseNearestContextRule(),
];

Expand Down Expand Up @@ -83,5 +88,10 @@ class SolidLintsPlugin extends Plugin {
UseNearestContextRule.code,
ReplaceWithNearestContextParameterFix.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,222 @@
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/dart/ast/token.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 text of a parameter (including leading comments and
/// indentation) and an optional trailing comment on the same line.
typedef _ParamBlock = ({String text, 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, :firstBlockStart) = _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;
return [
e.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 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> blocks, int firstBlockStart}) _extractParamBlocks(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can extract 3 helpers into some util in a separate file

  String? _getTrailingComment(
    int i,
    List<FormalParameter> namedParams,
    int upperBound,
    FormalParameter param,
  ) {
    final nextParamStart = i < namedParams.length - 1
        ? namedParams[i + 1].offset
        : upperBound;

    if (param.end >= nextParamStart) return null;

    final sameLine = utils
        .getTextRange(param.end, nextParamStart)
        .split('\n')
        .first;

    final commentIdx = sameLine.indexOf('//');

    return commentIdx != -1 ? sameLine.substring(commentIdx) : null;
  }

  Token? _getLeadingComment(
    FormalParameter param,
    int i,
    List<FormalParameter> namedParams,
  ) {
    Token? leadingComment = param.beginToken.precedingComments;
    if (i <= 0) return leadingComment;

    while (leadingComment != null) {
      final betweenText = utils.getTextRange(
        namedParams[i - 1].end,
        leadingComment.offset,
      );

      if (betweenText.contains('\n')) return leadingComment;

      leadingComment = leadingComment.next;
    }

    return leadingComment;
  }

  int _getBlockStart(
    FormalParameter param,
    Token? leadingComment,
    int minOffset,
  ) {
    final blockStart =
        leadingComment != null &&
            leadingComment.offset >= minOffset &&
            leadingComment.offset < param.offset
        ? leadingComment.offset
        : param.offset;

    final lineStart = utils.getLineThis(blockStart);
    final prefixText = utils.getTextRange(lineStart, blockStart);

    return prefixText.trim().isEmpty ? lineStart : blockStart;
  }

not sure about _getBlockStart naming - I'd thing of something more expressive

List<FormalParameter> namedParams,
FormalParameterList parameterList,
) {
final blocks = <_ParamBlock>[];
int? firstStart;

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;

// Find leading comment, skipping any trailing comment that belongs
// to the previous parameter (same line as previous param).
var blockStart = param.offset;
Token? leadingComment = param.beginToken.precedingComments;
if (i > 0) {
while (leadingComment != null) {
final betweenText = utils.getTextRange(
namedParams[i - 1].end,
leadingComment.offset,
);
if (!betweenText.contains('\n')) {
leadingComment = leadingComment.next;
} else {
break;
}
}
}
if (leadingComment != null &&
leadingComment.offset >= minOffset &&
leadingComment.offset < param.offset) {
blockStart = leadingComment.offset;
}
final lineStart = utils.getLineThis(blockStart);
final prefixText = utils.getTextRange(lineStart, blockStart);
if (prefixText.trim().isEmpty) {
blockStart = lineStart;
}

// Find trailing comment on the same line as this parameter.
String? trailingComment;
final nextParamStart = i < namedParams.length - 1
? namedParams[i + 1].offset
: upperBound;
if (param.end < nextParamStart) {
final afterParam = utils.getTextRange(param.end, nextParamStart);
final sameLine = afterParam.split('\n').first;
final commentIdx = sameLine.indexOf('//');
if (commentIdx != -1) {
trailingComment = sameLine.substring(commentIdx);
}
}

Comment thread
solid-illiaaihistov marked this conversation as resolved.
firstStart ??= blockStart;
blocks.add((
text: utils.getTextRange(blockStart, param.end),
trailingComment: trailingComment,
));
}

return (
blocks: blocks,
firstBlockStart: firstStart ?? namedParams.first.offset,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd think of maybe making the blocks list to be just indexes instead, so that we don't need that firstBlockVariable at all

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

      final leadingComment = _getLeadingComment(param, i, namedParams);
      final blockStart = _getBlockStart(param, leadingComment, minOffset);
      final trailingComment = _getTrailingComment(
        i,
        namedParams,
        upperBound,
        param,
      );

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

    return (
      blocks: blocks
          .map(
            (b) => (
              text: utils.getTextRange(b.start, b.end),
              trailingComment: b.trailingComment,
            ),
          )
          .toList(),
      firstBlockStart: blocks.first.start,
    );

The above is just a sketch, I think we should work towards

  List<({
    int blockStart,
    int blockEnd,
    String? trailingComment,
  })> _extractParamBlocks(
    List<FormalParameter> namedParams,
    FormalParameterList parameterList,
  ) {

);
}

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