Add ktlint test for generated files from templates (#167378)

Creates a new test in analyze.dart that will generate kotlin files from
.kt.tmpl and .kts.tmpl files and then run ktlint on them.

Fixes: #166497 

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Matt Boetger 2025-04-18 22:30:26 +00:00 committed by GitHub
parent 66ecc88655
commit c74cd04dbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 113 additions and 38 deletions

View File

@ -167,6 +167,9 @@ Future<void> run(List<String> arguments) async {
printProgress('Lint Kotlin files...');
await lintKotlinFiles(flutterRoot);
printProgress('Lint generated Kotlin files from templates...');
await lintKotlinTemplatedFiles(flutterRoot);
// Ensure that all package dependencies are in sync.
printProgress('Package dependencies...');
await runCommand(flutter, <String>[
@ -2460,6 +2463,66 @@ Future<void> verifyTabooDocumentation(String workingDirectory, {int minimumMatch
}
}
final Map<String, String> _kKotlinTemplateKeys = <String, String>{
'androidIdentifier': 'dummyPackage',
'pluginClass': 'PluginClass',
'projectName': 'dummy',
'agpVersion': '0.0.0.1',
'kotlinVersion': '0.0.0.1',
};
final String _kKotlinTemplateRelativePath = path.join('packages', 'flutter_tools', 'templates');
const List<String> _kKotlinExtList = <String>['.kt.tmpl', '.kts.tmpl'];
const String _kKotlinTmplExt = '.tmpl';
final RegExp _kKotlinTemplatePattern = RegExp(r'{{(.*?)}}');
/// Copy kotlin template files from [_kKotlinTemplateRelativePath] into a system tmp folder
/// then replace template values with values from [_kKotlinTemplateKeys] or "'dummy'" if an
/// unknown key is found. Then run ktlint on the tmp folder to check for lint errors in the
/// generated Kotlin files.
Future<void> lintKotlinTemplatedFiles(String workingDirectory) async {
final String templatePath = path.join(workingDirectory, _kKotlinTemplateRelativePath);
final Iterable<File> files = Directory(templatePath)
.listSync(recursive: true)
.toList()
.whereType<File>()
.where((File file) => _kKotlinExtList.contains(path.extension(file.path, 2)));
if (files.isEmpty) {
foundError(<String>['No Kotlin template files found']);
return;
}
final Directory tempDir = Directory.systemTemp.createTempSync('template_output');
for (final File templateFile in files) {
final String inputContent = await templateFile.readAsString();
final String modifiedContent = inputContent.replaceAllMapped(
_kKotlinTemplatePattern,
(Match match) => _kKotlinTemplateKeys[match[1]] ?? 'dummy',
);
String outputFilename = path.basename(templateFile.path);
outputFilename = outputFilename.substring(
0,
outputFilename.length - _kKotlinTmplExt.length,
); // Remove '.tmpl' from file path
// Ensure the first letter of the generated class is uppercase (instead of pluginClass)
outputFilename = outputFilename.substring(0, 1).toUpperCase() + outputFilename.substring(1);
final String relativePath = path.dirname(path.relative(templateFile.path, from: templatePath));
final String outputDir = path.join(tempDir.path, relativePath);
await Directory(outputDir).create(recursive: true);
final String outputFile = path.join(outputDir, outputFilename);
final File output = File(outputFile);
await output.writeAsString(modifiedContent);
}
return lintKotlinFiles(tempDir.path).whenComplete(() {
tempDir.deleteSync(recursive: true);
});
}
Future<void> lintKotlinFiles(String workingDirectory) async {
const String baselineRelativePath = 'dev/bots/test/analyze-test-input/ktlint-baseline.xml';
const String editorConfigRelativePath = 'dev/bots/test/analyze-test-input/.editorconfig';

View File

@ -5,7 +5,10 @@ allprojects {
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {

View File

@ -5,7 +5,10 @@ allprojects {
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {

View File

@ -1,11 +1,12 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

View File

@ -7,27 +7,32 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** {{pluginClass}} */
class {{pluginClass}}: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
class {{pluginClass}} :
FlutterPlugin,
MethodCallHandler {
// The MethodChannel that will the communication between Flutter and native Android
//
// This local reference serves to register the plugin with the Flutter Engine and unregister it
// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "{{projectName}}")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "{{projectName}}")
channel.setMethodCallHandler(this)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onMethodCall(
call: MethodCall,
result: Result
) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

View File

@ -2,8 +2,8 @@ package {{androidIdentifier}}
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.test.Test
import org.mockito.Mockito
import kotlin.test.Test
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
@ -14,14 +14,14 @@ import org.mockito.Mockito
*/
internal class {{pluginClass}}Test {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = {{pluginClass}}()
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = {{pluginClass}}()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}