Skip to content

Commit 5a7b16b

Browse files
srawlinsCommit Queue
authored and
Commit Queue
committed
DAS: introduce some assists that add digit separators to number literals
Work towards #56467 Thw two assists do the following: * Add a digit separators every 3 digits in a decimal int, a decimal double, and a scientific notation double. * Add a digit separators every 2 digits in a hexadecimal int. No assists are introduced which remove digit separators. Cq-Include-Trybots: luci.dart.try:flutter-analyze-try,analyzer-win-release-try,pkg-win-release-try Change-Id: I0920fb279285963c33a78d9288317213739ae83b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/380601 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
1 parent 95cbc6a commit 5a7b16b

File tree

5 files changed

+338
-0
lines changed

5 files changed

+338
-0
lines changed

pkg/analysis_server/lib/src/services/correction/assist.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ abstract final class DartAssistKind {
4747
DartAssistKindPriority.DEFAULT,
4848
'Add a debug reference to this property',
4949
);
50+
static const ADD_DIGIT_SEPARATORS = AssistKind(
51+
'dart.assist.add.addDigitSeparators',
52+
DartAssistKindPriority.DEFAULT,
53+
'Add digit separators',
54+
);
5055
static const ADD_RETURN_TYPE = AssistKind(
5156
'dart.assist.add.returnType',
5257
DartAssistKindPriority.DEFAULT,

pkg/analysis_server/lib/src/services/correction/assist_internal.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:analysis_server/plugin/edit/assist/assist_core.dart';
66
import 'package:analysis_server/plugin/edit/assist/assist_dart.dart';
77
import 'package:analysis_server/src/services/correction/dart/add_diagnostic_property_reference.dart';
8+
import 'package:analysis_server/src/services/correction/dart/add_digit_separators.dart';
89
import 'package:analysis_server/src/services/correction/dart/add_return_type.dart';
910
import 'package:analysis_server/src/services/correction/dart/add_type_annotation.dart';
1011
import 'package:analysis_server/src/services/correction/dart/assign_to_local_variable.dart';
@@ -87,6 +88,8 @@ class AssistProcessor {
8788
/// A list of the generators used to produce assists.
8889
static const List<ProducerGenerator> _generators = [
8990
AddDiagnosticPropertyReference.new,
91+
AddDigitSeparatorEveryThreeDigits.new,
92+
AddDigitSeparatorEveryTwoDigits.new,
9093
AddReturnType.new,
9194
AddTypeAnnotation.bulkFixable,
9295
AssignToLocalVariable.new,
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:_fe_analyzer_shared/src/parser/util.dart';
6+
import 'package:analysis_server/src/services/correction/assist.dart';
7+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
8+
import 'package:analyzer/dart/ast/ast.dart';
9+
import 'package:analyzer/dart/ast/token.dart';
10+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
11+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
12+
import 'package:analyzer_plugin/utilities/range_factory.dart';
13+
14+
class AddDigitSeparatorEveryThreeDigits extends _AddDigitSeparators {
15+
AddDigitSeparatorEveryThreeDigits({required super.context});
16+
17+
@override
18+
int get _digitsPerGroup => 3;
19+
20+
@override
21+
int get _minimumDigitCount => 5;
22+
23+
@override
24+
Set<TokenType> get _tokenTypes => const {
25+
TokenType.INT,
26+
TokenType.INT_WITH_SEPARATORS,
27+
TokenType.DOUBLE,
28+
TokenType.DOUBLE_WITH_SEPARATORS,
29+
};
30+
}
31+
32+
class AddDigitSeparatorEveryTwoDigits extends _AddDigitSeparators {
33+
AddDigitSeparatorEveryTwoDigits({required super.context});
34+
35+
@override
36+
int get _digitsPerGroup => 2;
37+
38+
@override
39+
int get _minimumDigitCount => 4;
40+
41+
@override
42+
Set<TokenType> get _tokenTypes =>
43+
const {TokenType.HEXADECIMAL, TokenType.HEXADECIMAL_WITH_SEPARATORS};
44+
}
45+
46+
abstract class _AddDigitSeparators extends ResolvedCorrectionProducer {
47+
_AddDigitSeparators({required super.context});
48+
49+
@override
50+
CorrectionApplicability get applicability =>
51+
CorrectionApplicability.automatically;
52+
53+
@override
54+
AssistKind get assistKind => DartAssistKind.ADD_DIGIT_SEPARATORS;
55+
56+
/// The number of digits in each group, to be separated by a separator.
57+
int get _digitsPerGroup;
58+
59+
/// The minimum number of digits in order to offer the correction.
60+
int get _minimumDigitCount;
61+
62+
/// The token types for which this correction is applicable.
63+
Set<TokenType> get _tokenTypes;
64+
65+
@override
66+
Future<void> compute(ChangeBuilder builder) async {
67+
var node = this.node;
68+
if (node is IntegerLiteral) {
69+
if (!_tokenTypes.contains(node.literal.type)) {
70+
return;
71+
}
72+
// We scrap whatever separators are in place.
73+
var source = stripSeparators(node.literal.lexeme);
74+
var isHex = node.literal.type == TokenType.HEXADECIMAL ||
75+
node.literal.type == TokenType.HEXADECIMAL_WITH_SEPARATORS;
76+
if (isHex) {
77+
const hexPrefixLength = '0x'.length;
78+
var replacement = _addSeparators(source.substring(hexPrefixLength));
79+
if (replacement == null) return;
80+
// Prefix the '0x' text.
81+
replacement = '${source.substring(0, hexPrefixLength)}$replacement';
82+
// Don't offer the correction if the result is unchanged.
83+
if (replacement == node.literal.lexeme) return;
84+
await builder.addDartFileEdit(file, (builder) {
85+
builder.addSimpleReplacement(range.node(node), replacement!);
86+
});
87+
} else {
88+
var replacement = _addSeparators(source);
89+
if (replacement == null) return;
90+
// Don't offer the correction if the result is unchanged.
91+
if (replacement == node.literal.lexeme) return;
92+
await builder.addDartFileEdit(file, (builder) {
93+
builder.addSimpleReplacement(range.node(node), replacement);
94+
});
95+
}
96+
} else if (node is DoubleLiteral) {
97+
if (!_tokenTypes.contains(node.literal.type)) {
98+
return;
99+
}
100+
// We scrap whatever separators are in place.
101+
var source = stripSeparators(node.literal.lexeme);
102+
103+
String wholePart;
104+
String? fractionalPart;
105+
String? exponentialPart;
106+
var exponentialPartIsNegative = false;
107+
108+
var eIndex = source.indexOf('e');
109+
if (eIndex == -1) eIndex = source.indexOf('E');
110+
if (eIndex > -1) {
111+
if (source.codeUnitAt(eIndex + 1) == 0x2D /* '-' */) {
112+
exponentialPartIsNegative = true;
113+
exponentialPart = source.substring(eIndex + 2);
114+
} else {
115+
exponentialPart = source.substring(eIndex + 1);
116+
}
117+
}
118+
119+
var decimalIndex = source.indexOf('.');
120+
if (decimalIndex > -1) {
121+
wholePart = source.substring(0, decimalIndex);
122+
fractionalPart = eIndex > -1
123+
? source.substring(decimalIndex + 1, eIndex)
124+
: source.substring(decimalIndex + 1);
125+
} else {
126+
assert(
127+
eIndex > -1,
128+
"There is neither a decimal point, nor an 'e', nor an 'E', in this "
129+
"double literal: '${node.literal.lexeme}'");
130+
wholePart = source.substring(0, eIndex);
131+
fractionalPart = null;
132+
}
133+
134+
var buffer = StringBuffer();
135+
var replacement = _addSeparators(wholePart);
136+
buffer.write(replacement ?? wholePart);
137+
138+
if (fractionalPart != null) {
139+
buffer.write('.');
140+
141+
// Reverse the fractional part, so that separators are aligned to the
142+
// left instead of the right.
143+
var fractionalPartReversed = fractionalPart.split('').reversed.join();
144+
replacement = _addSeparators(fractionalPartReversed);
145+
146+
// Reverse back the replacement.
147+
replacement = replacement?.split('').reversed.join();
148+
buffer.write(replacement ?? fractionalPart);
149+
}
150+
151+
if (exponentialPart != null) {
152+
buffer.write(source.substring(eIndex, eIndex + 1));
153+
if (exponentialPartIsNegative) buffer.write('-');
154+
replacement = _addSeparators(exponentialPart);
155+
buffer.write(replacement ?? exponentialPart);
156+
}
157+
158+
replacement = buffer.toString();
159+
// Don't offer the correction if the result is unchanged.
160+
if (replacement == node.literal.lexeme) return;
161+
162+
await builder.addDartFileEdit(file, (builder) {
163+
builder.addSimpleReplacement(range.node(node), buffer.toString());
164+
});
165+
}
166+
}
167+
168+
/// Adds separators to [digits], returning the result.
169+
String? _addSeparators(String digits) {
170+
// Only offer to add separators when the number has some minimum number of
171+
// digits.
172+
var digitCount = digits.length;
173+
if (digitCount < _minimumDigitCount) return null;
174+
175+
var buffer = StringBuffer();
176+
var offset = digitCount % _digitsPerGroup;
177+
if (offset == 0) offset = _digitsPerGroup;
178+
buffer.write(digits.substring(0, offset));
179+
offset += _digitsPerGroup;
180+
for (; offset <= digits.length; offset += _digitsPerGroup) {
181+
buffer.write('_');
182+
buffer.write(digits.substring(offset - _digitsPerGroup, offset));
183+
}
184+
return buffer.toString();
185+
}
186+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analysis_server/src/services/correction/assist.dart';
6+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
7+
import 'package:test_reflective_loader/test_reflective_loader.dart';
8+
9+
import 'assist_processor.dart';
10+
11+
void main() {
12+
defineReflectiveSuite(() {
13+
defineReflectiveTests(AddDigitSeparatorsTest);
14+
});
15+
}
16+
17+
@reflectiveTest
18+
class AddDigitSeparatorsTest extends AssistProcessorTest {
19+
@override
20+
AssistKind get kind => DartAssistKind.ADD_DIGIT_SEPARATORS;
21+
22+
Future<void> test_double_manyDigitsLeftOfDecimal() async {
23+
await resolveTestCode('var i = /*caret*/123456.78;');
24+
await assertHasAssist('var i = 123_456.78;');
25+
}
26+
27+
Future<void> test_double_multipleOfThreeDigits_rightOfDecimal() async {
28+
await resolveTestCode('var i = /*caret*/1.234567;');
29+
await assertHasAssist('var i = 1.234_567;');
30+
}
31+
32+
Future<void> test_double_multipleOfThreeDigitsPlusOne_rightOfDecimal() async {
33+
await resolveTestCode('var i = /*caret*/1.2345678;');
34+
await assertHasAssist('var i = 1.234_567_8;');
35+
}
36+
37+
Future<void> test_double_multipleOfThreeDigitsPlusTwo_rightOfDecimal() async {
38+
await resolveTestCode('var i = /*caret*/1.23456789;');
39+
await assertHasAssist('var i = 1.234_567_89;');
40+
}
41+
42+
Future<void> test_double_tooFewDigits() async {
43+
await resolveTestCode('var i = /*caret*/1234.5678;');
44+
await assertNoAssist();
45+
}
46+
47+
Future<void> test_doubleScientific_manyDigitsInExponential() async {
48+
await resolveTestCode('var i = /*caret*/1e23456;');
49+
await assertHasAssist('var i = 1e23_456;');
50+
}
51+
52+
Future<void> test_doubleScientific_manyDigitsInExponential_negative() async {
53+
await resolveTestCode('var i = /*caret*/1e-234567;');
54+
await assertHasAssist('var i = 1e-234_567;');
55+
}
56+
57+
Future<void>
58+
test_doubleScientific_manyDigitsInExponential_withFractional() async {
59+
await resolveTestCode('var i = /*caret*/1.2e34567;');
60+
await assertHasAssist('var i = 1.2e34_567;');
61+
}
62+
63+
Future<void> test_doubleScientific_manyDigitsInFractional() async {
64+
await resolveTestCode('var i = /*caret*/1.23456e7;');
65+
await assertHasAssist('var i = 1.234_56e7;');
66+
}
67+
68+
Future<void> test_doubleScientific_manyDigitsInWhole() async {
69+
await resolveTestCode('var i = /*caret*/12345e6;');
70+
await assertHasAssist('var i = 12_345e6;');
71+
}
72+
73+
Future<void> test_doubleScientific_manyDigitsInWhole_negative() async {
74+
await resolveTestCode('var i = /*caret*/12345e-6;');
75+
await assertHasAssist('var i = 12_345e-6;');
76+
}
77+
78+
Future<void> test_doubleScientific_manyDigitsInWhole_withFractional() async {
79+
await resolveTestCode('var i = /*caret*/12345.6e7;');
80+
await assertHasAssist('var i = 12_345.6e7;');
81+
}
82+
83+
Future<void> test_intDecimal_existingSeparators() async {
84+
await resolveTestCode('var i = /*caret*/123__456_78;');
85+
await assertHasAssist('var i = 12_345_678;');
86+
}
87+
88+
Future<void> test_intDecimal_existingSeparators_correct() async {
89+
await resolveTestCode('var i = /*caret*/12_345_678;');
90+
await assertNoAssist();
91+
}
92+
93+
Future<void> test_intDecimal_fourDigits() async {
94+
await resolveTestCode('var i = /*caret*/1234;');
95+
await assertNoAssist();
96+
}
97+
98+
Future<void> test_intDecimal_multipleOfThreeDigits() async {
99+
await resolveTestCode('var i = /*caret*/123456;');
100+
await assertHasAssist('var i = 123_456;');
101+
}
102+
103+
Future<void> test_intDecimal_multipleOfThreeDigitsPlusOne() async {
104+
await resolveTestCode('var i = /*caret*/1234567;');
105+
await assertHasAssist('var i = 1_234_567;');
106+
}
107+
108+
Future<void> test_intDecimal_multipleOfThreeDigitsPlusTwo() async {
109+
await resolveTestCode('var i = /*caret*/12345678;');
110+
await assertHasAssist('var i = 12_345_678;');
111+
}
112+
113+
Future<void> test_intDecimal_negativeNumber() async {
114+
await resolveTestCode('var i = -/*caret*/12345678;');
115+
await assertHasAssist('var i = -12_345_678;');
116+
}
117+
118+
Future<void> test_intHex_evenNumberOfDigits() async {
119+
await resolveTestCode('var i = /*caret*/0x123456;');
120+
await assertHasAssist('var i = 0x12_34_56;');
121+
}
122+
123+
Future<void> test_intHex_existingSeparators() async {
124+
await resolveTestCode('var i = /*caret*/0X1___234__5_6;');
125+
await assertHasAssist('var i = 0X12_34_56;');
126+
}
127+
128+
Future<void> test_intHex_existingSeparators_correct() async {
129+
await resolveTestCode('var i = /*caret*/0x12_34_56;');
130+
await assertNoAssist();
131+
}
132+
133+
Future<void> test_intHex_threeDigits() async {
134+
await resolveTestCode('var i = /*caret*/0x123;');
135+
await assertNoAssist();
136+
}
137+
138+
Future<void> test_intHex_upperCase() async {
139+
await resolveTestCode('var i = /*caret*/0X123456;');
140+
await assertHasAssist('var i = 0X12_34_56;');
141+
}
142+
}

pkg/analysis_server/test/src/services/correction/assist/test_all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:test_reflective_loader/test_reflective_loader.dart';
66

77
import 'add_diagnostic_property_reference_test.dart' as add_diagnostic_property;
8+
import 'add_digit_separators_test.dart' as add_digit_separators;
89
import 'add_return_type_test.dart' as add_return_type;
910
import 'add_type_annotation_test.dart' as add_type_annotation;
1011
import 'assign_to_local_variable_test.dart' as assign_to_local_variable;
@@ -99,6 +100,7 @@ import 'use_curly_braces_test.dart' as use_curly_braces;
99100
void main() {
100101
defineReflectiveSuite(() {
101102
add_diagnostic_property.main();
103+
add_digit_separators.main();
102104
add_return_type.main();
103105
add_type_annotation.main();
104106
assign_to_local_variable.main();

0 commit comments

Comments
 (0)