Skip to content

Commit 371d1fd

Browse files
committed
Allow all lint tasks to be up-to-date and cacheable
- Also test against AGP 3.2.0 - Acknowledge newer lint tasks actualy define input files
1 parent 4b9ae31 commit 371d1fd

File tree

4 files changed

+154
-37
lines changed

4 files changed

+154
-37
lines changed

src/integTest/groovy/com/monits/gradle/sca/AndroidLintIntegTest.groovy

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import static org.junit.Assert.assertThat
3535
class AndroidLintIntegTest extends AbstractIntegTestFixture {
3636

3737
static final List<String> ANDROID_PLUGIN_VERSIONS = (['1.1.3', '1.2.3', '1.3.1', '1.5.0', '2.0.0', '2.1.3'] +
38-
(Jvm.current.java8Compatible ? ['2.2.3', '2.3.3', '3.0.1'] : [])).asImmutable()
38+
(Jvm.current.java8Compatible ? ['2.2.3', '2.3.3', '3.0.1', '3.2.0'] : [])).asImmutable()
3939

4040
@SuppressWarnings('MethodName')
4141
@Unroll('AndroidLint should run when using gradle #version')
@@ -113,6 +113,8 @@ class AndroidLintIntegTest extends AbstractIntegTestFixture {
113113
BuildResult secondRun = gradleRunner.build()
114114

115115
then:
116+
file('build/outputs/').list().each { println it }
117+
116118
firstRun.task(taskName()).outcome == SUCCESS
117119
secondRun.task(taskName()).outcome == UP_TO_DATE
118120

@@ -124,6 +126,37 @@ class AndroidLintIntegTest extends AbstractIntegTestFixture {
124126
gradleVersion = gradleVersionForAndroid(androidVersion)
125127
}
126128

129+
@SuppressWarnings('MethodName')
130+
@Unroll('AndroidLint per variant re-run is up-to-date when using plugin version #androidVersion')
131+
void 'rerun is up-to-date for variants'() {
132+
given:
133+
writeAndroidBuildFile(androidVersion)
134+
useSimpleAndroidLintConfig()
135+
writeAndroidManifest()
136+
goodCode()
137+
138+
when:
139+
GradleRunner gradleRunner = gradleRunner()
140+
.withGradleVersion(gradleVersion)
141+
// plugin version 1.1.x failed to compile tests if assemble was not called beforehand
142+
.withArguments('assemble', 'lintDebug', 'lintRelease', '--stacktrace')
143+
BuildResult firstRun = gradleRunner.build()
144+
BuildResult secondRun = gradleRunner.build()
145+
146+
then:
147+
file('build/outputs/').list().each { println it }
148+
149+
firstRun.task(':lintDebug').outcome == SUCCESS
150+
secondRun.task(':lintDebug').outcome == UP_TO_DATE
151+
152+
firstRun.task(':lintRelease').outcome == SUCCESS
153+
secondRun.task(':lintRelease').outcome == UP_TO_DATE
154+
155+
where:
156+
androidVersion << ANDROID_PLUGIN_VERSIONS
157+
gradleVersion = gradleVersionForAndroid(androidVersion)
158+
}
159+
127160
@SuppressWarnings('MethodName')
128161
@Unroll('AndroidLint is skipped when disabled and using plugin version #androidVersion')
129162
void 'task is skipped if disabled'() {

src/integTest/groovy/com/monits/gradle/sca/fixture/AbstractIntegTestFixture.groovy

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ abstract class AbstractIntegTestFixture extends Specification {
144144
}
145145

146146
TestFile writeAndroidBuildFile(Map<String, Object> toolsConfig) {
147+
String extraRepository = toolsConfig.get(ANDROID_VERSION, DEFAULT_ANDROID_VERSION).startsWith('3') ? 'google()'
148+
: ''
149+
147150
buildScriptFile() << """
148151
|buildscript {
149152
| dependencies {
@@ -154,12 +157,13 @@ abstract class AbstractIntegTestFixture extends Specification {
154157
|
155158
| repositories {
156159
| jcenter()
157-
| ${toolsConfig.get(ANDROID_VERSION, DEFAULT_ANDROID_VERSION).startsWith('3') ? 'google()' : ''}
160+
| ${extraRepository}
158161
| }
159162
|}
160163
|
161164
|repositories {
162-
| mavenCentral()
165+
| jcenter()
166+
| ${extraRepository}
163167
|}
164168
|
165169
|apply plugin: 'com.android.library'

src/main/groovy/com/monits/gradle/sca/AndroidHelper.groovy

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,22 @@ final class AndroidHelper {
2727
private static final String ANDROID_ENABLE_CACHE_PROPERTY = 'android.enableBuildCache'
2828
private static final String ANDROID_CACHE_LOCATION = 'android.buildCacheDir'
2929
private static final String ANDROID_DEPENDENCY_PATTERN = /com\.android\.tools\.build\/gradle\/([^\/]+)/
30-
private static final VersionNumber BUILD_CACHE_ANDROID_GRADLE_VERSION = VersionNumber.parse('2.3.0')
31-
private static final VersionNumber REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION = VersionNumber.parse('2.0.0')
30+
private static final String VERSION_2_3_0 = '2.3.0'
31+
32+
private static final VersionNumber BUILD_CACHE_ANDROID_GRADLE_VERSION = VersionNumber.parse(VERSION_2_3_0)
33+
private static final VersionNumber REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION_MIN = VersionNumber.parse('2.0.0')
34+
private static final VersionNumber REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION_MAX = VersionNumber.parse('2.2.9')
35+
private static final VersionNumber LINT_HAS_VARIANT_INFO = VersionNumber.parse('1.5.0')
36+
private static final VersionNumber USES_REPORTS_DIR = VersionNumber.parse(VERSION_2_3_0)
3237

3338
/**
34-
* Checks if the current Android Plugin produces a report per variant or not.
39+
* Checks if the current Android Plugin produces a global report that matches a debuggable variant or not.
3540
* @param project The project to analyze.
3641
* @return True if a report per variant is expected, false otherwise
3742
*/
38-
static boolean lintReportPerVariant(final Project project) {
39-
getCurrentVersion(project) >= REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION
43+
static boolean globalLintIsVariant(final Project project) {
44+
getCurrentVersion(project) >= REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION_MIN &&
45+
getCurrentVersion(project) <= REPORT_PER_VARIANT_ANDROID_GRADLE_VERSION_MAX
4046
}
4147

4248
/**
@@ -53,6 +59,30 @@ final class AndroidHelper {
5359
project.property(ANDROID_ENABLE_CACHE_PROPERTY) == 'true')
5460
}
5561

62+
/**
63+
* Checks if the current Android build has variant info on the lint task.
64+
*
65+
* @param project The project to analyze
66+
* @return True if the AGP version in use provides variant info on the lint task, false otherwise
67+
*/
68+
static boolean lintTaskHasVariantInfo(final Project project) {
69+
getCurrentVersion(project) >= LINT_HAS_VARIANT_INFO
70+
}
71+
72+
/**
73+
* Retrieves the location were lint reports are output by AGP.
74+
*
75+
* @param project The project to analyze
76+
* @return The directory in which AGP outputs lint reports
77+
*/
78+
static String getLintReportDir(final Project project) {
79+
if (getCurrentVersion(project) >= USES_REPORTS_DIR) {
80+
return "${project.buildDir}/reports/"
81+
}
82+
83+
"${project.buildDir}/outputs/"
84+
}
85+
5686
/**
5787
* Retrieves the location of Android's build-cache directory.
5888
* @param project The project to analyze.

src/main/groovy/com/monits/gradle/sca/config/AndroidLintConfigurator.groovy

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.gradle.api.DomainObjectSet
2323
import org.gradle.api.Project
2424
import org.gradle.api.Task
2525
import org.gradle.api.specs.Specs
26+
import org.gradle.api.tasks.Copy
2627
import org.gradle.util.GradleVersion
2728

2829
/**
@@ -35,6 +36,9 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
3536
private static final String JACK_OPTIONS_PROPERTY_NAME = 'jackOptions'
3637
private static final String ANDROID = 'android'
3738
private static final String LINT_OPTIONS = 'lintOptions'
39+
private static final String GLOBAL_LINT_TASK_NAME = 'lint'
40+
private static final String XML = 'xml'
41+
private static final String HTML = 'html'
3842

3943
private final RemoteConfigLocator configLocator = new RemoteConfigLocator(ANDROID)
4044

@@ -45,17 +49,38 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
4549

4650
@Override
4751
void applyAndroidConfig(final Project project, final StaticCodeAnalysisExtension extension) {
48-
project.tasks.matching { Task it -> it.name == 'lint' } .all { Task t ->
52+
Class<? extends Task> lintTask = getLintTaskClass(project)
53+
project.tasks.withType(lintTask) { Task t ->
4954
setupTasks(t, project, extension)
5055

5156
configureLintTask(project, extension, t)
5257
}
5358
}
5459

60+
private static Class<? extends Task> getLintTaskClass(final Project project) {
61+
try {
62+
// AGP 3.0+
63+
return getClass().classLoader
64+
.loadClass('com.android.build.gradle.tasks.LintBaseTask') as Class<? extends Task>
65+
} catch (ClassNotFoundException ignored) {
66+
try {
67+
// Older versions
68+
return getClass().classLoader
69+
.loadClass('com.android.build.gradle.tasks.Lint') as Class<? extends Task>
70+
} catch (ClassNotFoundException e) {
71+
// Something went wrong!
72+
warnUnexpectedException(project, 'Encountered an error trying to configure Android Lint tasks.', e)
73+
74+
// Best effort, get the global lint task class (needs to be configured!)
75+
project.tasks.getByName(GLOBAL_LINT_TASK_NAME).class
76+
}
77+
}
78+
}
79+
5580
private static void setupTasks(final Task lintTask, final Project project,
5681
final StaticCodeAnalysisExtension extension) {
57-
Task resolveTask = project.tasks.create('resolveAndroidLint', ResolveAndroidLintTask)
58-
Task cleanupTask = project.tasks.create('cleanupAndroidLint', CleanupAndroidLintTask)
82+
Task resolveTask = project.tasks.maybeCreate('resolveAndroidLint', ResolveAndroidLintTask)
83+
Task cleanupTask = project.tasks.maybeCreate('cleanupAndroidLint', CleanupAndroidLintTask)
5984

6085
lintTask.dependsOn resolveTask
6186
lintTask.finalizedBy cleanupTask
@@ -71,14 +96,12 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
7196
final File configSource, final Task lintTask) {
7297
def lintOptions = project[ANDROID][LINT_OPTIONS]
7398

99+
// update global config
74100
lintOptions.with { it ->
75101
// TODO : This won't fail on warnings, just like Checkstyle.
76102
// See https://issues.gradle.org/browse/GRADLE-2888
77103
it['abortOnError'] = !extension.ignoreErrors
78104

79-
// Change output location for consistency with other plugins
80-
it['xmlOutput'] = project.file("${project.buildDir}/reports/android/lint-results.xml")
81-
82105
// Update global config
83106
it['lintConfig'] = configSource
84107
}
@@ -94,6 +117,15 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
94117

95118
configureLintOptions(project, extension, config, lintTask)
96119

120+
if (lintTask.name == GLOBAL_LINT_TASK_NAME) {
121+
// Change output location for consistency with other plugins
122+
// we copy as to not tamper with other lint tasks
123+
lintTask.finalizedBy(project.tasks.create('copyLintReport', Copy) { Copy it ->
124+
it.from lintTask.outputs.files.filter { File f -> f.name.endsWith('.xml') }
125+
it.into project.file("${project.buildDir}/reports/android/lint-results.xml")
126+
})
127+
}
128+
97129
try {
98130
configureLintInputsAndOutputs(project, lintTask)
99131

@@ -103,9 +135,8 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
103135
}
104136
} catch (Throwable e) {
105137
// Something went wrong!
106-
project.logger.warn('Encountered an error trying to set inputs and outputs for Android Lint ' +
107-
'tasks, it will be disabled. Please, report this incident in ' +
108-
'https://github.com/monits/static-code-analysis-plugin/issues', e)
138+
warnUnexpectedException(project, 'Encountered an error trying to set inputs and outputs for Android Lint ' +
139+
'tasks, it will be disabled.', e)
109140

110141
// disable up-to-date caching
111142
lintTask.outputs.upToDateWhen {
@@ -114,6 +145,11 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
114145
}
115146
}
116147

148+
private static void warnUnexpectedException(final Project project, final String message, final Throwable e) {
149+
project.logger.warn(message + ' Please, report this incident at ' +
150+
'https://github.com/monits/static-code-analysis-plugin/issues', e)
151+
}
152+
117153
private File obtainLintRules(final Project project, final StaticCodeAnalysisExtension config,
118154
final Task lintTask) {
119155
boolean remoteLocation = RemoteConfigLocator.isRemoteLocation(config.androidLintConfig)
@@ -150,27 +186,41 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
150186

151187
DomainObjectSet<?> variants = getVariants(project)
152188

153-
String defaultReportVariant = null
154-
variants.all {
155-
def configuration = it.variantData.variantConfiguration
156-
String variantName = it.name
157-
String variantDirName = configuration.dirName
158-
159-
lintTask.inputs.with {
160-
dir("${project.buildDir}/intermediates/classes/${variantDirName}/")
161-
dir("${project.buildDir}/intermediates/assets/${variantDirName}/")
162-
dir("${project.buildDir}/intermediates/manifests/full/${variantDirName}/")
163-
dir("${project.buildDir}/intermediates/res/merged/${variantDirName}/")
164-
dir("${project.buildDir}/intermediates/shaders/${variantDirName}/")
165-
dir("${project.buildDir}/intermediates/rs/${variantDirName}/")
189+
String variantName = AndroidHelper.lintTaskHasVariantInfo(project) ? lintTask.variantName
190+
: (lintTask.name.toLowerCase() - GLOBAL_LINT_TASK_NAME)
191+
192+
// Older plugins didn't setup input files, so up-to-date checks were futile
193+
if (lintTask.inputs.files.isEmpty()) {
194+
variants.matching { it.name == variantName || variantName == null || variantName.empty }.all {
195+
def configuration = it.variantData.variantConfiguration
196+
String variantDirName = configuration.dirName
197+
198+
lintTask.inputs.with {
199+
dir("${project.buildDir}/intermediates/classes/${variantDirName}/")
200+
dir("${project.buildDir}/intermediates/assets/${variantDirName}/")
201+
dir("${project.buildDir}/intermediates/manifests/full/${variantDirName}/")
202+
dir("${project.buildDir}/intermediates/res/merged/${variantDirName}/")
203+
dir("${project.buildDir}/intermediates/shaders/${variantDirName}/")
204+
dir("${project.buildDir}/intermediates/rs/${variantDirName}/")
205+
}
166206
}
207+
}
167208

168-
if (!defaultReportVariant && configuration.buildType.isDebuggable() && !usesJack(configuration)) {
169-
defaultReportVariant = variantName
209+
// And none up to this date setup outputs for up-to-date checks and cache
210+
if ((variantName == null || variantName.empty) && AndroidHelper.globalLintIsVariant(project)) {
211+
boolean configFound = false
212+
variants.all {
213+
def configuration = it.variantData.variantConfiguration
214+
if (!configFound && configuration.buildType.isDebuggable() && !usesJack(configuration)) {
215+
configFound = true
170216

171-
addReportAsOutput(lintTask, project, xmlEnabled, xmlOutput, defaultReportVariant, 'xml')
172-
addReportAsOutput(lintTask, project, htmlEnabled, htmlOutput, defaultReportVariant, 'html')
217+
addReportAsOutput(lintTask, project, xmlEnabled, xmlOutput, it.name, XML)
218+
addReportAsOutput(lintTask, project, htmlEnabled, htmlOutput, it.name, HTML)
219+
}
173220
}
221+
} else {
222+
addReportAsOutput(lintTask, project, xmlEnabled, xmlOutput, variantName, XML)
223+
addReportAsOutput(lintTask, project, htmlEnabled, htmlOutput, variantName, HTML)
174224
}
175225
}
176226

@@ -231,11 +281,11 @@ class AndroidLintConfigurator implements AnalysisConfigurator {
231281
File definiteOutput = output
232282
if (!output) {
233283
// Convention naming changed along the way
234-
if (AndroidHelper.lintReportPerVariant(project)) {
284+
if (variantName) {
235285
definiteOutput = project.file(
236-
"${project.buildDir}/outputs/lint-results-${variantName}.${extension}")
286+
AndroidHelper.getLintReportDir(project) + "lint-results-${variantName}.${extension}")
237287
} else {
238-
definiteOutput = project.file("${project.buildDir}/outputs/lint-results.${extension}")
288+
definiteOutput = project.file(AndroidHelper.getLintReportDir(project) + "lint-results.${extension}")
239289
}
240290
}
241291
task.outputs.file definiteOutput

0 commit comments

Comments
 (0)