1// Copyright 2020 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package java
16
17import (
18	"fmt"
19	"sort"
20	"strings"
21
22	"android/soong/android"
23)
24
25type LintProperties struct {
26	// Controls for running Android Lint on the module.
27	Lint struct {
28
29		// If true, run Android Lint on the module.  Defaults to true.
30		Enabled *bool
31
32		// Flags to pass to the Android Lint tool.
33		Flags []string
34
35		// Checks that should be treated as fatal.
36		Fatal_checks []string
37
38		// Checks that should be treated as errors.
39		Error_checks []string
40
41		// Checks that should be treated as warnings.
42		Warning_checks []string
43
44		// Checks that should be skipped.
45		Disabled_checks []string
46
47		// Modules that provide extra lint checks
48		Extra_check_modules []string
49	}
50}
51
52type linter struct {
53	name                string
54	manifest            android.Path
55	mergedManifest      android.Path
56	srcs                android.Paths
57	srcJars             android.Paths
58	resources           android.Paths
59	classpath           android.Paths
60	classes             android.Path
61	extraLintCheckJars  android.Paths
62	test                bool
63	library             bool
64	minSdkVersion       string
65	targetSdkVersion    string
66	compileSdkVersion   string
67	javaLanguageLevel   string
68	kotlinLanguageLevel string
69	outputs             lintOutputs
70	properties          LintProperties
71
72	reports android.Paths
73
74	buildModuleReportZip bool
75}
76
77type lintOutputs struct {
78	html android.Path
79	text android.Path
80	xml  android.Path
81
82	depSets LintDepSets
83}
84
85type lintOutputsIntf interface {
86	lintOutputs() *lintOutputs
87}
88
89type lintDepSetsIntf interface {
90	LintDepSets() LintDepSets
91}
92
93type LintDepSets struct {
94	HTML, Text, XML *android.DepSet
95}
96
97type LintDepSetsBuilder struct {
98	HTML, Text, XML *android.DepSetBuilder
99}
100
101func NewLintDepSetBuilder() LintDepSetsBuilder {
102	return LintDepSetsBuilder{
103		HTML: android.NewDepSetBuilder(android.POSTORDER),
104		Text: android.NewDepSetBuilder(android.POSTORDER),
105		XML:  android.NewDepSetBuilder(android.POSTORDER),
106	}
107}
108
109func (l LintDepSetsBuilder) Direct(html, text, xml android.Path) LintDepSetsBuilder {
110	l.HTML.Direct(html)
111	l.Text.Direct(text)
112	l.XML.Direct(xml)
113	return l
114}
115
116func (l LintDepSetsBuilder) Transitive(depSets LintDepSets) LintDepSetsBuilder {
117	if depSets.HTML != nil {
118		l.HTML.Transitive(depSets.HTML)
119	}
120	if depSets.Text != nil {
121		l.Text.Transitive(depSets.Text)
122	}
123	if depSets.XML != nil {
124		l.XML.Transitive(depSets.XML)
125	}
126	return l
127}
128
129func (l LintDepSetsBuilder) Build() LintDepSets {
130	return LintDepSets{
131		HTML: l.HTML.Build(),
132		Text: l.Text.Build(),
133		XML:  l.XML.Build(),
134	}
135}
136
137func (l *linter) LintDepSets() LintDepSets {
138	return l.outputs.depSets
139}
140
141var _ lintDepSetsIntf = (*linter)(nil)
142
143var _ lintOutputsIntf = (*linter)(nil)
144
145func (l *linter) lintOutputs() *lintOutputs {
146	return &l.outputs
147}
148
149func (l *linter) enabled() bool {
150	return BoolDefault(l.properties.Lint.Enabled, true)
151}
152
153func (l *linter) deps(ctx android.BottomUpMutatorContext) {
154	if !l.enabled() {
155		return
156	}
157
158	extraCheckModules := l.properties.Lint.Extra_check_modules
159
160	if checkOnly := ctx.Config().Getenv("ANDROID_LINT_CHECK"); checkOnly != "" {
161		if checkOnlyModules := ctx.Config().Getenv("ANDROID_LINT_CHECK_EXTRA_MODULES"); checkOnlyModules != "" {
162			extraCheckModules = strings.Split(checkOnlyModules, ",")
163		}
164	}
165
166	ctx.AddFarVariationDependencies(ctx.Config().BuildOSCommonTarget.Variations(),
167		extraLintCheckTag, extraCheckModules...)
168}
169
170func (l *linter) writeLintProjectXML(ctx android.ModuleContext,
171	rule *android.RuleBuilder) (projectXMLPath, configXMLPath, cacheDir, homeDir android.WritablePath, deps android.Paths) {
172
173	var resourcesList android.WritablePath
174	if len(l.resources) > 0 {
175		// The list of resources may be too long to put on the command line, but
176		// we can't use the rsp file because it is already being used for srcs.
177		// Insert a second rule to write out the list of resources to a file.
178		resourcesList = android.PathForModuleOut(ctx, "lint", "resources.list")
179		resListRule := android.NewRuleBuilder()
180		resListRule.Command().Text("cp").FlagWithRspFileInputList("", l.resources).Output(resourcesList)
181		resListRule.Build(pctx, ctx, "lint_resources_list", "lint resources list")
182		deps = append(deps, l.resources...)
183	}
184
185	projectXMLPath = android.PathForModuleOut(ctx, "lint", "project.xml")
186	// Lint looks for a lint.xml file next to the project.xml file, give it one.
187	configXMLPath = android.PathForModuleOut(ctx, "lint", "lint.xml")
188	cacheDir = android.PathForModuleOut(ctx, "lint", "cache")
189	homeDir = android.PathForModuleOut(ctx, "lint", "home")
190
191	srcJarDir := android.PathForModuleOut(ctx, "lint-srcjars")
192	srcJarList := zipSyncCmd(ctx, rule, srcJarDir, l.srcJars)
193
194	cmd := rule.Command().
195		BuiltTool(ctx, "lint-project-xml").
196		FlagWithOutput("--project_out ", projectXMLPath).
197		FlagWithOutput("--config_out ", configXMLPath).
198		FlagWithArg("--name ", ctx.ModuleName())
199
200	if l.library {
201		cmd.Flag("--library")
202	}
203	if l.test {
204		cmd.Flag("--test")
205	}
206	if l.manifest != nil {
207		deps = append(deps, l.manifest)
208		cmd.FlagWithArg("--manifest ", l.manifest.String())
209	}
210	if l.mergedManifest != nil {
211		deps = append(deps, l.mergedManifest)
212		cmd.FlagWithArg("--merged_manifest ", l.mergedManifest.String())
213	}
214
215	// TODO(ccross): some of the files in l.srcs are generated sources and should be passed to
216	// lint separately.
217	cmd.FlagWithRspFileInputList("--srcs ", l.srcs)
218	deps = append(deps, l.srcs...)
219
220	cmd.FlagWithInput("--generated_srcs ", srcJarList)
221	deps = append(deps, l.srcJars...)
222
223	if resourcesList != nil {
224		cmd.FlagWithInput("--resources ", resourcesList)
225	}
226
227	if l.classes != nil {
228		deps = append(deps, l.classes)
229		cmd.FlagWithArg("--classes ", l.classes.String())
230	}
231
232	cmd.FlagForEachArg("--classpath ", l.classpath.Strings())
233	deps = append(deps, l.classpath...)
234
235	cmd.FlagForEachArg("--extra_checks_jar ", l.extraLintCheckJars.Strings())
236	deps = append(deps, l.extraLintCheckJars...)
237
238	cmd.FlagWithArg("--root_dir ", "$PWD")
239
240	// The cache tag in project.xml is relative to the root dir, or the project.xml file if
241	// the root dir is not set.
242	cmd.FlagWithArg("--cache_dir ", cacheDir.String())
243
244	cmd.FlagWithInput("@",
245		android.PathForSource(ctx, "build/soong/java/lint_defaults.txt"))
246
247	cmd.FlagForEachArg("--disable_check ", l.properties.Lint.Disabled_checks)
248	cmd.FlagForEachArg("--warning_check ", l.properties.Lint.Warning_checks)
249	cmd.FlagForEachArg("--error_check ", l.properties.Lint.Error_checks)
250	cmd.FlagForEachArg("--fatal_check ", l.properties.Lint.Fatal_checks)
251
252	return projectXMLPath, configXMLPath, cacheDir, homeDir, deps
253}
254
255// generateManifest adds a command to the rule to write a simple manifest that contains the
256// minSdkVersion and targetSdkVersion for modules (like java_library) that don't have a manifest.
257func (l *linter) generateManifest(ctx android.ModuleContext, rule *android.RuleBuilder) android.Path {
258	manifestPath := android.PathForModuleOut(ctx, "lint", "AndroidManifest.xml")
259
260	rule.Command().Text("(").
261		Text(`echo "<?xml version='1.0' encoding='utf-8'?>" &&`).
262		Text(`echo "<manifest xmlns:android='http://schemas.android.com/apk/res/android'" &&`).
263		Text(`echo "    android:versionCode='1' android:versionName='1' >" &&`).
264		Textf(`echo "  <uses-sdk android:minSdkVersion='%s' android:targetSdkVersion='%s'/>" &&`,
265			l.minSdkVersion, l.targetSdkVersion).
266		Text(`echo "</manifest>"`).
267		Text(") >").Output(manifestPath)
268
269	return manifestPath
270}
271
272func (l *linter) lint(ctx android.ModuleContext) {
273	if !l.enabled() {
274		return
275	}
276
277	extraLintCheckModules := ctx.GetDirectDepsWithTag(extraLintCheckTag)
278	for _, extraLintCheckModule := range extraLintCheckModules {
279		if dep, ok := extraLintCheckModule.(Dependency); ok {
280			l.extraLintCheckJars = append(l.extraLintCheckJars, dep.ImplementationAndResourcesJars()...)
281		} else {
282			ctx.PropertyErrorf("lint.extra_check_modules",
283				"%s is not a java module", ctx.OtherModuleName(extraLintCheckModule))
284		}
285	}
286
287	rule := android.NewRuleBuilder()
288
289	if l.manifest == nil {
290		manifest := l.generateManifest(ctx, rule)
291		l.manifest = manifest
292	}
293
294	projectXML, lintXML, cacheDir, homeDir, deps := l.writeLintProjectXML(ctx, rule)
295
296	html := android.PathForModuleOut(ctx, "lint-report.html")
297	text := android.PathForModuleOut(ctx, "lint-report.txt")
298	xml := android.PathForModuleOut(ctx, "lint-report.xml")
299
300	depSetsBuilder := NewLintDepSetBuilder().Direct(html, text, xml)
301
302	ctx.VisitDirectDepsWithTag(staticLibTag, func(dep android.Module) {
303		if depLint, ok := dep.(lintDepSetsIntf); ok {
304			depSetsBuilder.Transitive(depLint.LintDepSets())
305		}
306	})
307
308	rule.Command().Text("rm -rf").Flag(cacheDir.String()).Flag(homeDir.String())
309	rule.Command().Text("mkdir -p").Flag(cacheDir.String()).Flag(homeDir.String())
310
311	var annotationsZipPath, apiVersionsXMLPath android.Path
312	if ctx.Config().UnbundledBuildUsePrebuiltSdks() {
313		annotationsZipPath = android.PathForSource(ctx, "prebuilts/sdk/current/public/data/annotations.zip")
314		apiVersionsXMLPath = android.PathForSource(ctx, "prebuilts/sdk/current/public/data/api-versions.xml")
315	} else {
316		annotationsZipPath = copiedAnnotationsZipPath(ctx)
317		apiVersionsXMLPath = copiedAPIVersionsXmlPath(ctx)
318	}
319
320	cmd := rule.Command().
321		Text("(").
322		Flag("JAVA_OPTS=-Xmx2048m").
323		FlagWithArg("ANDROID_SDK_HOME=", homeDir.String()).
324		FlagWithInput("SDK_ANNOTATIONS=", annotationsZipPath).
325		FlagWithInput("LINT_OPTS=-DLINT_API_DATABASE=", apiVersionsXMLPath).
326		Tool(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/bin/lint")).
327		Implicit(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/lib/lint-classpath.jar")).
328		Flag("--quiet").
329		FlagWithInput("--project ", projectXML).
330		FlagWithInput("--config ", lintXML).
331		FlagWithOutput("--html ", html).
332		FlagWithOutput("--text ", text).
333		FlagWithOutput("--xml ", xml).
334		FlagWithArg("--compile-sdk-version ", l.compileSdkVersion).
335		FlagWithArg("--java-language-level ", l.javaLanguageLevel).
336		FlagWithArg("--kotlin-language-level ", l.kotlinLanguageLevel).
337		FlagWithArg("--url ", fmt.Sprintf(".=.,%s=out", android.PathForOutput(ctx).String())).
338		Flag("--exitcode").
339		Flags(l.properties.Lint.Flags).
340		Implicits(deps)
341
342	if checkOnly := ctx.Config().Getenv("ANDROID_LINT_CHECK"); checkOnly != "" {
343		cmd.FlagWithArg("--check ", checkOnly)
344	}
345
346	cmd.Text("|| (").Text("cat").Input(text).Text("; exit 7)").Text(")")
347
348	rule.Command().Text("rm -rf").Flag(cacheDir.String()).Flag(homeDir.String())
349
350	rule.Build(pctx, ctx, "lint", "lint")
351
352	l.outputs = lintOutputs{
353		html: html,
354		text: text,
355		xml:  xml,
356
357		depSets: depSetsBuilder.Build(),
358	}
359
360	if l.buildModuleReportZip {
361		l.reports = BuildModuleLintReportZips(ctx, l.LintDepSets())
362	}
363}
364
365func BuildModuleLintReportZips(ctx android.ModuleContext, depSets LintDepSets) android.Paths {
366	htmlList := depSets.HTML.ToSortedList()
367	textList := depSets.Text.ToSortedList()
368	xmlList := depSets.XML.ToSortedList()
369
370	if len(htmlList) == 0 && len(textList) == 0 && len(xmlList) == 0 {
371		return nil
372	}
373
374	htmlZip := android.PathForModuleOut(ctx, "lint-report-html.zip")
375	lintZip(ctx, htmlList, htmlZip)
376
377	textZip := android.PathForModuleOut(ctx, "lint-report-text.zip")
378	lintZip(ctx, textList, textZip)
379
380	xmlZip := android.PathForModuleOut(ctx, "lint-report-xml.zip")
381	lintZip(ctx, xmlList, xmlZip)
382
383	return android.Paths{htmlZip, textZip, xmlZip}
384}
385
386type lintSingleton struct {
387	htmlZip android.WritablePath
388	textZip android.WritablePath
389	xmlZip  android.WritablePath
390}
391
392func (l *lintSingleton) GenerateBuildActions(ctx android.SingletonContext) {
393	l.generateLintReportZips(ctx)
394	l.copyLintDependencies(ctx)
395}
396
397func (l *lintSingleton) copyLintDependencies(ctx android.SingletonContext) {
398	if ctx.Config().UnbundledBuildUsePrebuiltSdks() {
399		return
400	}
401
402	var frameworkDocStubs android.Module
403	ctx.VisitAllModules(func(m android.Module) {
404		if ctx.ModuleName(m) == "framework-doc-stubs" {
405			if frameworkDocStubs == nil {
406				frameworkDocStubs = m
407			} else {
408				ctx.Errorf("lint: multiple framework-doc-stubs modules found: %s and %s",
409					ctx.ModuleSubDir(m), ctx.ModuleSubDir(frameworkDocStubs))
410			}
411		}
412	})
413
414	if frameworkDocStubs == nil {
415		if !ctx.Config().AllowMissingDependencies() {
416			ctx.Errorf("lint: missing framework-doc-stubs")
417		}
418		return
419	}
420
421	ctx.Build(pctx, android.BuildParams{
422		Rule:   android.Cp,
423		Input:  android.OutputFileForModule(ctx, frameworkDocStubs, ".annotations.zip"),
424		Output: copiedAnnotationsZipPath(ctx),
425	})
426
427	ctx.Build(pctx, android.BuildParams{
428		Rule:   android.Cp,
429		Input:  android.OutputFileForModule(ctx, frameworkDocStubs, ".api_versions.xml"),
430		Output: copiedAPIVersionsXmlPath(ctx),
431	})
432}
433
434func copiedAnnotationsZipPath(ctx android.PathContext) android.WritablePath {
435	return android.PathForOutput(ctx, "lint", "annotations.zip")
436}
437
438func copiedAPIVersionsXmlPath(ctx android.PathContext) android.WritablePath {
439	return android.PathForOutput(ctx, "lint", "api_versions.xml")
440}
441
442func (l *lintSingleton) generateLintReportZips(ctx android.SingletonContext) {
443	if ctx.Config().UnbundledBuild() {
444		return
445	}
446
447	var outputs []*lintOutputs
448	var dirs []string
449	ctx.VisitAllModules(func(m android.Module) {
450		if ctx.Config().EmbeddedInMake() && !m.ExportedToMake() {
451			return
452		}
453
454		if apex, ok := m.(android.ApexModule); ok && apex.NotAvailableForPlatform() && apex.IsForPlatform() {
455			// There are stray platform variants of modules in apexes that are not available for
456			// the platform, and they sometimes can't be built.  Don't depend on them.
457			return
458		}
459
460		if l, ok := m.(lintOutputsIntf); ok {
461			outputs = append(outputs, l.lintOutputs())
462		}
463	})
464
465	dirs = android.SortedUniqueStrings(dirs)
466
467	zip := func(outputPath android.WritablePath, get func(*lintOutputs) android.Path) {
468		var paths android.Paths
469
470		for _, output := range outputs {
471			if p := get(output); p != nil {
472				paths = append(paths, p)
473			}
474		}
475
476		lintZip(ctx, paths, outputPath)
477	}
478
479	l.htmlZip = android.PathForOutput(ctx, "lint-report-html.zip")
480	zip(l.htmlZip, func(l *lintOutputs) android.Path { return l.html })
481
482	l.textZip = android.PathForOutput(ctx, "lint-report-text.zip")
483	zip(l.textZip, func(l *lintOutputs) android.Path { return l.text })
484
485	l.xmlZip = android.PathForOutput(ctx, "lint-report-xml.zip")
486	zip(l.xmlZip, func(l *lintOutputs) android.Path { return l.xml })
487
488	ctx.Phony("lint-check", l.htmlZip, l.textZip, l.xmlZip)
489}
490
491func (l *lintSingleton) MakeVars(ctx android.MakeVarsContext) {
492	if !ctx.Config().UnbundledBuild() {
493		ctx.DistForGoal("lint-check", l.htmlZip, l.textZip, l.xmlZip)
494	}
495}
496
497var _ android.SingletonMakeVarsProvider = (*lintSingleton)(nil)
498
499func init() {
500	android.RegisterSingletonType("lint",
501		func() android.Singleton { return &lintSingleton{} })
502}
503
504func lintZip(ctx android.BuilderContext, paths android.Paths, outputPath android.WritablePath) {
505	paths = android.SortedUniquePaths(android.CopyOfPaths(paths))
506
507	sort.Slice(paths, func(i, j int) bool {
508		return paths[i].String() < paths[j].String()
509	})
510
511	rule := android.NewRuleBuilder()
512
513	rule.Command().BuiltTool(ctx, "soong_zip").
514		FlagWithOutput("-o ", outputPath).
515		FlagWithArg("-C ", android.PathForIntermediates(ctx).String()).
516		FlagWithRspFileInputList("-l ", paths)
517
518	rule.Build(pctx, ctx, outputPath.Base(), outputPath.Base())
519}
520