From ca7ddc9dcb2b815d9bbdfabe165cf3f777112207 Mon Sep 17 00:00:00 2001 From: Alan Knight Date: Thu, 19 Oct 2023 16:37:59 -0400 Subject: [PATCH 1/5] Experiment at a fast codemod executable invokeable from IDE extensions --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 00556fc7..9670fa0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ executables: rmui_preparation: rmui_bundle_update: intl_message_migration: + intl_quick: dependency_validator: ignore: - meta From 4036cc7a13078eb65667cb1ca3299fbfd094ab0f Mon Sep 17 00:00:00 2001 From: Alan Knight Date: Thu, 19 Oct 2023 16:44:40 -0400 Subject: [PATCH 2/5] include the files --- bin/intl_quick.dart | 15 +++ lib/src/executables/intl_quick_migration.dart | 104 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 bin/intl_quick.dart create mode 100644 lib/src/executables/intl_quick_migration.dart diff --git a/bin/intl_quick.dart b/bin/intl_quick.dart new file mode 100644 index 00000000..9ab89709 --- /dev/null +++ b/bin/intl_quick.dart @@ -0,0 +1,15 @@ +// Copyright 2021 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'package:over_react_codemod/src/executables/intl_quick_migration.dart'; diff --git a/lib/src/executables/intl_quick_migration.dart b/lib/src/executables/intl_quick_migration.dart new file mode 100644 index 00000000..ed94744e --- /dev/null +++ b/lib/src/executables/intl_quick_migration.dart @@ -0,0 +1,104 @@ +// Copyright 2021 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:codemod/codemod.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:over_react_codemod/src/intl_suggestors/intl_messages.dart'; +import 'package:over_react_codemod/src/intl_suggestors/intl_migrator.dart'; +import 'package:path/path.dart' as p; + +typedef Migrator = Stream Function(FileContext); + +final FileSystem fs = const LocalFileSystem(); + +final parser = ArgParser() + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Prints this help output.', + ) + ..addFlag( + 'verbose', + abbr: 'v', + negatable: false, + help: 'Outputs all logging to stdout/stderr.', + ); + +late ArgResults parsedArgs; + +void main(List args) async { + parsedArgs = parser.parse(args); + if (parsedArgs['help'] as bool) { + printUsage(); + return; + } + + if (parsedArgs.rest.isEmpty) { + print('You have to specify a file'); + exit(1); + } + var intlPath = p.canonicalize(p.absolute((parsedArgs.rest.first))); + + await migratePackage(fs.currentDirectory.path, intlPath); +} + +void printUsage() { + stderr.writeln('Migrates a particular string to an intl message.'); + stderr.writeln(); + stderr.writeln('Usage:'); + stderr.writeln(' intl_quick [arguments]'); + stderr.writeln(); + stderr.writeln('Options:'); + stderr.writeln(parser.usage); +} + +/// Migrate files included in [paths] within [packagePath]. +/// +/// We expect [paths] to be absolute. +Future migratePackage(String packagePath, String path) async { + final packageName = p.basename(packagePath); + + final IntlMessages messages = IntlMessages(packageName, + directory: fs.currentDirectory, packagePath: packagePath); + + exitCode = await runMigrators([path], [], messages, packageName); + + messages.write(force: false); + // This will leave the intl.dart file unformatted, but that takes too long, so we'll just leave it out. +} + +Future runMigrators(List packageDartPaths, + List codemodArgs, IntlMessages messages, String packageName) async { + // final intlPropMigrator = IntlMigrator(messages.className, messages); + final constantStringMigrator = + ConstantStringMigrator(messages.className, messages); + // final importMigrator = (FileContext context) => + // intlImporter(context, packageName, messages.className); + + // List> migrators = [ + // [intlPropMigrator], +// [constantStringMigrator], + // [importMigrator], + // ]; + + var result = await runInteractiveCodemod( + packageDartPaths, constantStringMigrator, + defaultYes: true); + return result; +} From f2072805791d8b745b1867fd5738ea09bc818eeb Mon Sep 17 00:00:00 2001 From: Alan Knight Date: Fri, 20 Oct 2023 10:21:33 -0400 Subject: [PATCH 3/5] A simple single-string migrator --- lib/src/executables/intl_quick_migration.dart | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/lib/src/executables/intl_quick_migration.dart b/lib/src/executables/intl_quick_migration.dart index ed94744e..66313d57 100644 --- a/lib/src/executables/intl_quick_migration.dart +++ b/lib/src/executables/intl_quick_migration.dart @@ -14,12 +14,15 @@ import 'dart:io'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; import 'package:args/args.dart'; import 'package:codemod/codemod.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; +import 'package:over_react_codemod/src/intl_suggestors/intl_importer.dart'; import 'package:over_react_codemod/src/intl_suggestors/intl_messages.dart'; -import 'package:over_react_codemod/src/intl_suggestors/intl_migrator.dart'; +import 'package:over_react_codemod/src/intl_suggestors/utils.dart'; import 'package:path/path.dart' as p; typedef Migrator = Stream Function(FileContext); @@ -85,20 +88,52 @@ Future migratePackage(String packagePath, String path) async { Future runMigrators(List packageDartPaths, List codemodArgs, IntlMessages messages, String packageName) async { - // final intlPropMigrator = IntlMigrator(messages.className, messages); - final constantStringMigrator = - ConstantStringMigrator(messages.className, messages); - // final importMigrator = (FileContext context) => - // intlImporter(context, packageName, messages.className); - - // List> migrators = [ - // [intlPropMigrator], -// [constantStringMigrator], - // [importMigrator], - // ]; - - var result = await runInteractiveCodemod( - packageDartPaths, constantStringMigrator, + final constantStringMigrator = SingleStringMigrator(messages, 1, 1); + final importMigrator = (FileContext context) => + intlImporter(context, packageName, messages.className); + + var result = await runInteractiveCodemodSequence( + packageDartPaths, [constantStringMigrator, importMigrator], defaultYes: true); return result; } + +class SingleStringMigrator extends GeneralizingAstVisitor + with AstVisitingSuggestor { + final IntlMessages _messages; + int startPosition; + int endPosition; + + SingleStringMigrator(this._messages, this.startPosition, this.endPosition); + + @override + visitStringLiteral(StringLiteral node) { + // Assume this is a single character position and just check if it's within the string for now. + if (node.offset <= startPosition && node.end >= startPosition) { + migrateStringExpression(node); + } + super.visitStringLiteral(node); + } + + void migrateStringExpression(StringLiteral node) { + var stringForm = stringContent(node); + if (stringForm != null && stringForm.isNotEmpty) { + final functionCall = + _messages.syntax.getterCall(node, _messages.className); + final functionDef = + _messages.syntax.getterDefinition(node, _messages.className); + yieldPatch(functionCall, node.offset, node.end); + addMethodToClass(_messages, functionDef); + } else { + if (isValidStringInterpolationNode(node)) { + var interpolation = node as StringInterpolation; + final functionCall = _messages.syntax + .functionCall(interpolation, _messages.className, ''); + final functionDef = _messages.syntax + .functionDefinition(interpolation, _messages.className, ''); + yieldPatch(functionCall, interpolation.offset, interpolation.end); + addMethodToClass(_messages, functionDef); + } + } + } +} From db79a2f23a7d7cc317a25aee4baee6e1e3e43542 Mon Sep 17 00:00:00 2001 From: Alan Knight Date: Fri, 20 Oct 2023 11:05:41 -0400 Subject: [PATCH 4/5] Minimal tests --- lib/src/executables/intl_quick_migration.dart | 1 + .../single_string_migrator_test.dart | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 test/intl_suggestors/single_string_migrator_test.dart diff --git a/lib/src/executables/intl_quick_migration.dart b/lib/src/executables/intl_quick_migration.dart index 66313d57..280ee393 100644 --- a/lib/src/executables/intl_quick_migration.dart +++ b/lib/src/executables/intl_quick_migration.dart @@ -98,6 +98,7 @@ Future runMigrators(List packageDartPaths, return result; } +// TODO: This shouldn't be in the executable file. class SingleStringMigrator extends GeneralizingAstVisitor with AstVisitingSuggestor { final IntlMessages _messages; diff --git a/test/intl_suggestors/single_string_migrator_test.dart b/test/intl_suggestors/single_string_migrator_test.dart new file mode 100644 index 00000000..a39bba32 --- /dev/null +++ b/test/intl_suggestors/single_string_migrator_test.dart @@ -0,0 +1,80 @@ +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:over_react_codemod/src/executables/intl_quick_migration.dart'; +import 'package:over_react_codemod/src/intl_suggestors/intl_messages.dart'; +import 'package:test/test.dart'; + +import '../resolved_file_context.dart'; +import '../util.dart'; + +void main() { + final resolvedContext = SharedAnalysisContext.overReact; + + // Warm up analysis in a setUpAll so that if getting the resolved AST times out + // (which is more common for the WSD context), it fails here instead of failing the first test. + setUpAll(resolvedContext.warmUpAnalysis); + + group('Single string Migrator', () { + final FileSystem fs = MemoryFileSystem(); + late IntlMessages messages; + late SuggestorTester basicSuggestor; + + // Idempotency isn't a worry for this suggestor, and testing it throws off + // checking for duplicates, so disable it for these tests. + // TODO: Avoid duplicating this between test files. + Future testSuggestor( + {required String input, required String expectedOutput}) => + basicSuggestor( + input: input, + expectedOutput: expectedOutput, + testIdempotency: false); + + setUp(() async { + final Directory tmp = await fs.systemTempDirectory.createTemp(); + messages = IntlMessages('TestClass', directory: tmp); + messages.outputFile.createSync(recursive: true); + }); + + suggest(int characterPosition) { + basicSuggestor = getSuggestorTester( + SingleStringMigrator(messages, characterPosition, characterPosition), + resolvedContext: resolvedContext, + ); + } + + tearDown(() { + messages.delete(); + }); + + group('Constants', () { + test('standlalone variable', () async { + suggest(26); + await testSuggestor( + input: ''' + var foo = 'I am a user-visible constant'; + ''', + expectedOutput: ''' + var foo = TestClassIntl.iAmAUservisibleConstant; + ''', + ); + final expectedFileContent = + '\n static String get iAmAUservisibleConstant => Intl.message(\'I am a user-visible constant\', name: \'TestClassIntl_iAmAUservisibleConstant\');\n'; + expect(messages.messageContents(), expectedFileContent); + }); + + test('out of range', () async { + suggest(15); + await testSuggestor( + input: ''' + var foo = 'I am a user-visible constant'; + ''', + expectedOutput: ''' + var foo = 'I am a user-visible constant'; + ''', + ); + final expectedFileContent = ''; + expect(messages.messageContents(), expectedFileContent); + }); + }); + }); +} From 5aca2634e8cf9cd37de4a5ecba3ea03827a9de06 Mon Sep 17 00:00:00 2001 From: Alan Knight Date: Fri, 20 Oct 2023 14:38:07 -0400 Subject: [PATCH 5/5] Pass the character offset --- lib/src/executables/intl_quick_migration.dart | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/src/executables/intl_quick_migration.dart b/lib/src/executables/intl_quick_migration.dart index 280ee393..22eed0ce 100644 --- a/lib/src/executables/intl_quick_migration.dart +++ b/lib/src/executables/intl_quick_migration.dart @@ -36,6 +36,12 @@ final parser = ArgParser() negatable: false, help: 'Prints this help output.', ) + ..addOption( + 'offset', + abbr: 'o', + help: + 'The character offset of a position within the string we want to migrate', + ) ..addFlag( 'verbose', abbr: 'v', @@ -58,7 +64,8 @@ void main(List args) async { } var intlPath = p.canonicalize(p.absolute((parsedArgs.rest.first))); - await migratePackage(fs.currentDirectory.path, intlPath); + await migratePackage( + fs.currentDirectory.path, intlPath, int.parse(parsedArgs['offset'])); } void printUsage() { @@ -74,27 +81,33 @@ void printUsage() { /// Migrate files included in [paths] within [packagePath]. /// /// We expect [paths] to be absolute. -Future migratePackage(String packagePath, String path) async { +Future migratePackage(String packagePath, String path, int offset) async { final packageName = p.basename(packagePath); final IntlMessages messages = IntlMessages(packageName, directory: fs.currentDirectory, packagePath: packagePath); - exitCode = await runMigrators([path], [], messages, packageName); + exitCode = await runMigrators( + [path], ['--yes-to-all'], messages, packageName, offset); messages.write(force: false); // This will leave the intl.dart file unformatted, but that takes too long, so we'll just leave it out. } -Future runMigrators(List packageDartPaths, - List codemodArgs, IntlMessages messages, String packageName) async { - final constantStringMigrator = SingleStringMigrator(messages, 1, 1); +Future runMigrators( + List packageDartPaths, + List codemodArgs, + IntlMessages messages, + String packageName, + int offset) async { + final constantStringMigrator = SingleStringMigrator(messages, offset, offset); + // The import migrator is extremely slow, probably looking at all the files. final importMigrator = (FileContext context) => intlImporter(context, packageName, messages.className); var result = await runInteractiveCodemodSequence( - packageDartPaths, [constantStringMigrator, importMigrator], - defaultYes: true); + packageDartPaths, [constantStringMigrator], + args: ['--yes-to-all'], defaultYes: true); return result; }