From 790d844b511f5b8b2e7b7a04647752a3e3e2070e Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Fri, 7 Nov 2025 05:26:20 -0800 Subject: [PATCH] [analyzer_utilities] Add String.toSnakeCase extension method. This extension method complements the existing `toCamelCase` and `toPascalCase` extension methods, allowing a string to be converted from `camelCase` or `PascalCase` to `snake_case`. This extension method will be useful in the analyzer diagnostic message generation logic, which still uses a mix of case styles. Note that there's no need for a separate method to convert to `UPPER_SNAKE_CASE`; that can be easily done by calling `toUpperCase` after `toSnakeCase`. Change-Id: I6a6a69647f4ee176ecdbaefc3de0906a88c69079 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/460206 Reviewed-by: Johnni Winther Commit-Queue: Paul Berry --- .../lib/extensions/string.dart | 24 +++++++++++++++++++ .../test/extensions/string_test.dart | 16 +++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pkg/analyzer_utilities/lib/extensions/string.dart b/pkg/analyzer_utilities/lib/extensions/string.dart index db526b1158b..49180149eed 100644 --- a/pkg/analyzer_utilities/lib/extensions/string.dart +++ b/pkg/analyzer_utilities/lib/extensions/string.dart @@ -3,6 +3,8 @@ // BSD-style license that can be found in the LICENSE file. extension StringExtension on String { + static final _toSnakeCaseRegExp = RegExp('_?[A-Z]'); + /// Converts `SCREAMING_SNAKE_CASE` or `snake_case` to `camelCase`. String toCamelCase() { var parts = toLowerCase().split('_'); @@ -51,4 +53,26 @@ extension StringExtension on String { } return buffer.toString(); } + + /// Converts `camelCase` or `PascalCase` to `snake_case` + String toSnakeCase() { + var parts = []; + var i = 0; + var wordStarts = _toSnakeCaseRegExp.allMatches(this); + for (var RegExpMatch(:start) in wordStarts) { + if (i < start) { + parts.add(substring(i, start).toLowerCase()); + i = start; + } + if (this[i] == '_' && parts.isNotEmpty) { + // Avoid doubling up the `_`. This handles strings that are already in + // snake case like `foo_Bar` (which translates to `foo_bar`). + i++; + } + } + if (i < length) { + parts.add(substring(i).toLowerCase()); + } + return parts.join('_'); + } } diff --git a/pkg/analyzer_utilities/test/extensions/string_test.dart b/pkg/analyzer_utilities/test/extensions/string_test.dart index 6eda8133860..c8a17d5022d 100644 --- a/pkg/analyzer_utilities/test/extensions/string_test.dart +++ b/pkg/analyzer_utilities/test/extensions/string_test.dart @@ -41,4 +41,20 @@ class StringExtensionTest { expect('FOO__BAR'.toPascalCase(), 'FooBar'); expect('FOO_BAR_'.toPascalCase(), 'FooBar'); } + + void test_toSnakeCase() { + expect('camelCase'.toSnakeCase(), 'camel_case'); + expect('PascalCase'.toSnakeCase(), 'pascal_case'); + expect('already_snake_case'.toSnakeCase(), 'already_snake_case'); + expect( + 'mixedCamel_AndPascal_and_snake'.toSnakeCase(), + 'mixed_camel_and_pascal_and_snake', + ); + expect('with123Numbers'.toSnakeCase(), 'with123_numbers'); + expect(''.toSnakeCase(), ''); + expect( + 'CONSECUTIVE_UPCASE'.toSnakeCase(), + 'c_o_n_s_e_c_u_t_i_v_e_u_p_c_a_s_e', + ); + } }