Skip to content

Commit c74cd04

Browse files
authored
Add ktlint test for generated files from templates (flutter#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: flutter#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
1 parent 66ecc88 commit c74cd04

File tree

6 files changed

+112
-37
lines changed

6 files changed

+112
-37
lines changed

dev/bots/analyze.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ Future<void> run(List<String> arguments) async {
167167
printProgress('Lint Kotlin files...');
168168
await lintKotlinFiles(flutterRoot);
169169

170+
printProgress('Lint generated Kotlin files from templates...');
171+
await lintKotlinTemplatedFiles(flutterRoot);
172+
170173
// Ensure that all package dependencies are in sync.
171174
printProgress('Package dependencies...');
172175
await runCommand(flutter, <String>[
@@ -2460,6 +2463,66 @@ Future<void> verifyTabooDocumentation(String workingDirectory, {int minimumMatch
24602463
}
24612464
}
24622465

2466+
final Map<String, String> _kKotlinTemplateKeys = <String, String>{
2467+
'androidIdentifier': 'dummyPackage',
2468+
'pluginClass': 'PluginClass',
2469+
'projectName': 'dummy',
2470+
'agpVersion': '0.0.0.1',
2471+
'kotlinVersion': '0.0.0.1',
2472+
};
2473+
2474+
final String _kKotlinTemplateRelativePath = path.join('packages', 'flutter_tools', 'templates');
2475+
2476+
const List<String> _kKotlinExtList = <String>['.kt.tmpl', '.kts.tmpl'];
2477+
const String _kKotlinTmplExt = '.tmpl';
2478+
final RegExp _kKotlinTemplatePattern = RegExp(r'{{(.*?)}}');
2479+
2480+
/// Copy kotlin template files from [_kKotlinTemplateRelativePath] into a system tmp folder
2481+
/// then replace template values with values from [_kKotlinTemplateKeys] or "'dummy'" if an
2482+
/// unknown key is found. Then run ktlint on the tmp folder to check for lint errors in the
2483+
/// generated Kotlin files.
2484+
Future<void> lintKotlinTemplatedFiles(String workingDirectory) async {
2485+
final String templatePath = path.join(workingDirectory, _kKotlinTemplateRelativePath);
2486+
final Iterable<File> files = Directory(templatePath)
2487+
.listSync(recursive: true)
2488+
.toList()
2489+
.whereType<File>()
2490+
.where((File file) => _kKotlinExtList.contains(path.extension(file.path, 2)));
2491+
2492+
if (files.isEmpty) {
2493+
foundError(<String>['No Kotlin template files found']);
2494+
return;
2495+
}
2496+
2497+
final Directory tempDir = Directory.systemTemp.createTempSync('template_output');
2498+
for (final File templateFile in files) {
2499+
final String inputContent = await templateFile.readAsString();
2500+
final String modifiedContent = inputContent.replaceAllMapped(
2501+
_kKotlinTemplatePattern,
2502+
(Match match) => _kKotlinTemplateKeys[match[1]] ?? 'dummy',
2503+
);
2504+
2505+
String outputFilename = path.basename(templateFile.path);
2506+
outputFilename = outputFilename.substring(
2507+
0,
2508+
outputFilename.length - _kKotlinTmplExt.length,
2509+
); // Remove '.tmpl' from file path
2510+
2511+
// Ensure the first letter of the generated class is uppercase (instead of pluginClass)
2512+
outputFilename = outputFilename.substring(0, 1).toUpperCase() + outputFilename.substring(1);
2513+
2514+
final String relativePath = path.dirname(path.relative(templateFile.path, from: templatePath));
2515+
final String outputDir = path.join(tempDir.path, relativePath);
2516+
await Directory(outputDir).create(recursive: true);
2517+
final String outputFile = path.join(outputDir, outputFilename);
2518+
final File output = File(outputFile);
2519+
await output.writeAsString(modifiedContent);
2520+
}
2521+
return lintKotlinFiles(tempDir.path).whenComplete(() {
2522+
tempDir.deleteSync(recursive: true);
2523+
});
2524+
}
2525+
24632526
Future<void> lintKotlinFiles(String workingDirectory) async {
24642527
const String baselineRelativePath = 'dev/bots/test/analyze-test-input/ktlint-baseline.xml';
24652528
const String editorConfigRelativePath = 'dev/bots/test/analyze-test-input/.editorconfig';

packages/flutter_tools/templates/app/android-java.tmpl/build.gradle.kts.tmpl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ allprojects {
55
}
66
}
77

8-
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
8+
val newBuildDir: Directory =
9+
rootProject.layout.buildDirectory
10+
.dir("../../build")
11+
.get()
912
rootProject.layout.buildDirectory.value(newBuildDir)
1013

1114
subprojects {

packages/flutter_tools/templates/app/android-kotlin.tmpl/build.gradle.kts.tmpl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ allprojects {
55
}
66
}
77

8-
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
8+
val newBuildDir: Directory =
9+
rootProject.layout.buildDirectory
10+
.dir("../../build")
11+
.get()
912
rootProject.layout.buildDirectory.value(newBuildDir)
1013

1114
subprojects {

packages/flutter_tools/templates/app/android.tmpl/settings.gradle.kts.tmpl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
pluginManagement {
2-
val flutterSdkPath = run {
3-
val properties = java.util.Properties()
4-
file("local.properties").inputStream().use { properties.load(it) }
5-
val flutterSdkPath = properties.getProperty("flutter.sdk")
6-
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
7-
flutterSdkPath
8-
}
2+
val flutterSdkPath =
3+
run {
4+
val properties = java.util.Properties()
5+
file("local.properties").inputStream().use { properties.load(it) }
6+
val flutterSdkPath = properties.getProperty("flutter.sdk")
7+
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
8+
flutterSdkPath
9+
}
910

1011
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
1112

packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,32 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
77
import io.flutter.plugin.common.MethodChannel.Result
88

99
/** {{pluginClass}} */
10-
class {{pluginClass}}: FlutterPlugin, MethodCallHandler {
11-
/// The MethodChannel that will the communication between Flutter and native Android
12-
///
13-
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
14-
/// when the Flutter Engine is detached from the Activity
15-
private lateinit var channel : MethodChannel
10+
class {{pluginClass}} :
11+
FlutterPlugin,
12+
MethodCallHandler {
13+
// The MethodChannel that will the communication between Flutter and native Android
14+
//
15+
// This local reference serves to register the plugin with the Flutter Engine and unregister it
16+
// when the Flutter Engine is detached from the Activity
17+
private lateinit var channel: MethodChannel
1618

17-
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
18-
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "{{projectName}}")
19-
channel.setMethodCallHandler(this)
20-
}
19+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
20+
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "{{projectName}}")
21+
channel.setMethodCallHandler(this)
22+
}
2123

22-
override fun onMethodCall(call: MethodCall, result: Result) {
23-
if (call.method == "getPlatformVersion") {
24-
result.success("Android ${android.os.Build.VERSION.RELEASE}")
25-
} else {
26-
result.notImplemented()
24+
override fun onMethodCall(
25+
call: MethodCall,
26+
result: Result
27+
) {
28+
if (call.method == "getPlatformVersion") {
29+
result.success("Android ${android.os.Build.VERSION.RELEASE}")
30+
} else {
31+
result.notImplemented()
32+
}
2733
}
28-
}
2934

30-
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
31-
channel.setMethodCallHandler(null)
32-
}
35+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
36+
channel.setMethodCallHandler(null)
37+
}
3338
}

packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/test/kotlin/androidIdentifier/pluginClassTest.kt.tmpl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package {{androidIdentifier}}
22

33
import io.flutter.plugin.common.MethodCall
44
import io.flutter.plugin.common.MethodChannel
5-
import kotlin.test.Test
65
import org.mockito.Mockito
6+
import kotlin.test.Test
77

88
/*
99
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
@@ -14,14 +14,14 @@ import org.mockito.Mockito
1414
*/
1515

1616
internal class {{pluginClass}}Test {
17-
@Test
18-
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
19-
val plugin = {{pluginClass}}()
17+
@Test
18+
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
19+
val plugin = {{pluginClass}}()
2020

21-
val call = MethodCall("getPlatformVersion", null)
22-
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
23-
plugin.onMethodCall(call, mockResult)
21+
val call = MethodCall("getPlatformVersion", null)
22+
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
23+
plugin.onMethodCall(call, mockResult)
2424

25-
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
26-
}
25+
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
26+
}
2727
}

0 commit comments

Comments
 (0)