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,256 @@
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/source/source_range.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.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';

/// 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];
sortedNamedParams.sort((a, b) {
final typeA = ParameterType.fromParameter(a);
final typeB = ParameterType.fromParameter(b);
final indexA = parametersOrder.indexOf(typeA);
final indexB = parametersOrder.indexOf(typeB);
return indexA.compareTo(indexB);
});
Comment thread
solid-illiaaihistov marked this conversation as resolved.
Comment thread
solid-illiaaihistov marked this conversation as resolved.
Comment on lines +46 to +54

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.

Suggested change
final sortedNamedParams = [...namedParams];
sortedNamedParams.sort((a, b) {
final typeA = ParameterType.fromParameter(a);
final typeB = ParameterType.fromParameter(b);
final indexA = parametersOrder.indexOf(typeA);
final indexB = parametersOrder.indexOf(typeB);
return indexA.compareTo(indexB);
});
final sortedNamedParams = namedParams.sortedBy(
(e) => parametersOrder.indexOf(ParameterType.fromParameter(e)),
);


// Check if the order is already correct (if sorting changed nothing)
bool isChanged = false;
for (int i = 0; i < namedParams.length; i++) {
if (namedParams[i] != sortedNamedParams[i]) {
isChanged = true;
break;
}
}
Comment on lines +57 to +63

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.

Suggested change
bool isChanged = false;
for (int i = 0; i < namedParams.length; i++) {
if (namedParams[i] != sortedNamedParams[i]) {
isChanged = true;
break;
}
}
final isChanged = namedParams
.zipWith(sortedNamedParams)
.any((pair) => pair.$1 != pair.$2);
extension IterableZip<T> on Iterable<T> {
  Iterable<(T, T)> zipWith(Iterable<T> other) sync* {
    for (var i = 0; i < length && i < other.length; i++) {
      yield (elementAt(i), other.elementAt(i));
    }
  }
}

if (!isChanged) return;

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

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

if (!isMultiline && !hasComments) {
// Single-line: no leading comments, simple text replacement
final sortedTexts = sortedNamedParams
.map((p) => utils.getRangeText(p.sourceRange))
.toList();

final replacementText = sortedTexts.join(', ');

final targetRange = SourceRange(
namedParams.first.offset,
namedParams.last.end - namedParams.first.offset,
);

await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(targetRange, replacementText);
});
return;
}
Comment on lines +74 to +91

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.

sourceStart and sourceEnd are used later in code too

Suggested change
if (!isMultiline && !hasComments) {
// Single-line: no leading comments, simple text replacement
final sortedTexts = sortedNamedParams
.map((p) => utils.getRangeText(p.sourceRange))
.toList();
final replacementText = sortedTexts.join(', ');
final targetRange = SourceRange(
namedParams.first.offset,
namedParams.last.end - namedParams.first.offset,
);
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(targetRange, replacementText);
});
return;
}
final sourceStart = namedParams.first.offset;
final sourceEnd = namedParams.last.end;
if (!isMultiline && !hasComments) {
return builder.addDartFileEdit(file, (builder) {
builder.addSimpleReplacement(
SourceRange(sourceStart, sourceEnd - sourceStart),
sortedNamedParams
.map((p) => utils.getRangeText(p.sourceRange))
.toList()
.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 hasTrailingComma = namedParams.last.endToken.next?.lexeme == ',';

// Build replacement text preserving trailing comments
final buffer = StringBuffer();
for (int i = 0; i < sortedBlocks.length; i++) {
buffer.write(sortedBlocks[i].text);

final isLast = i == sortedBlocks.length - 1;
if (!isLast || hasTrailingComma) {
buffer.write(',');
}
final trailingComment = sortedBlocks[i].trailingComment;
if (trailingComment != null) {
buffer.write(' $trailingComment');
}
if (!isLast) {
buffer.write('\n');
}
}
Comment on lines +109 to +124

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.

Suggested change
final buffer = StringBuffer();
for (int i = 0; i < sortedBlocks.length; i++) {
buffer.write(sortedBlocks[i].text);
final isLast = i == sortedBlocks.length - 1;
if (!isLast || hasTrailingComma) {
buffer.write(',');
}
final trailingComment = sortedBlocks[i].trailingComment;
if (trailingComment != null) {
buffer.write(' $trailingComment');
}
if (!isLast) {
buffer.write('\n');
}
}
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 = namedParams.last.end;
if (hasTrailingComma) {
rangeEnd = namedParams.last.endToken.next!.end;

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 probably extract final tokenAfterEnd = namedParams.last.endToken.next; cuz it's used twice, and make this

    var rangeEnd = hasTrailingComma ? tokenAfterEnd!.end : sourceEnd;

}
final upperBound =
parameterList.rightDelimiter?.offset ??
parameterList.rightParenthesis.offset;
if (rangeEnd < upperBound) {
final afterLast = utils.getRangeText(
SourceRange(rangeEnd, upperBound - rangeEnd),

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 see that we are constantly doing SourceRange(start, end - start),

why don't we extract a simple utility SourceRange _createRange(int start, int end) => SourceRange(start, end - start);

Same stuff:
String _getRangeText(int start, int end) => utils.getRangeText(_createRange(start, end));

);
final newlineIdx = afterLast.indexOf('\n');
if (newlineIdx != -1) {
rangeEnd += newlineIdx;
}
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.

final targetRange = SourceRange(
firstBlockStart,
rangeEnd - firstBlockStart,
);

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

/// 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(
List<FormalParameter> namedParams,
FormalParameterList parameterList,
) {
final blocks = <_ParamBlock>[];
int? firstStart;

for (int i = 0; i < namedParams.length; i++) {
final param = namedParams[i];

final int minOffset = i == 0
? (parameterList.leftDelimiter?.end ??
parameterList.leftParenthesis.end)
: 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.getRangeText(
SourceRange(
namedParams[i - 1].end,
leadingComment.offset - namedParams[i - 1].end,
),
);
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.getRangeText(
SourceRange(lineStart, blockStart - lineStart),
);
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
: (parameterList.rightDelimiter?.offset ??
parameterList.rightParenthesis.offset);
if (param.end < nextParamStart) {
final afterParam = utils.getRangeText(
SourceRange(param.end, nextParamStart - param.end),
);
final newlineIdx = afterParam.indexOf('\n');
final sameLine = newlineIdx == -1
? afterParam
: afterParam.substring(0, newlineIdx);
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.getRangeText(
SourceRange(blockStart, param.end - blockStart),
),
trailingComment: trailingComment,
));
}

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

List<ParameterType> _getParametersOrder() {
final loader = AnalysisOptionsLoader(
resourceProvider: resourceProvider,
);
final options = loader.getRuleOptionsForFile(
file,
NamedParametersOrderingRule.lintName,
);
if (options != null) {
return NamedParametersOrderingParameters.fromJson(options).order;
}
return ParameterType.defaultOrder;
}
}
Loading