Add support for running Android lint on java and android modules.

Add a rule that runs Android lint on each java and android module
and produces reports in xml, html and text formats.

Bug: 153485543
Test: m out/soong/.intermediates/packages/apps/Settings/Settings-core/android_common/lint-report.html
Change-Id: I5a530975b73ba767fef45b257d4f9ec901a19fcb
diff --git a/java/Android.bp b/java/Android.bp
index 2de1b8e..1fda7f7 100644
--- a/java/Android.bp
+++ b/java/Android.bp
@@ -37,6 +37,7 @@
         "jdeps.go",
         "java_resources.go",
         "kotlin.go",
+        "lint.go",
         "platform_compat_config.go",
         "plugin.go",
         "prebuilt_apis.go",
diff --git a/java/aar.go b/java/aar.go
index 878704d..074ead4 100644
--- a/java/aar.go
+++ b/java/aar.go
@@ -102,6 +102,7 @@
 	sdkLibraries            []string
 	hasNoCode               bool
 	LoggingParent           string
+	resourceFiles           android.Paths
 
 	splitNames []string
 	splits     []split
@@ -275,6 +276,7 @@
 
 	var compiledResDirs []android.Paths
 	for _, dir := range resDirs {
+		a.resourceFiles = append(a.resourceFiles, dir.files...)
 		compiledResDirs = append(compiledResDirs, aapt2Compile(ctx, dir.dir, dir.files, compileFlags).Paths())
 	}
 
@@ -473,6 +475,10 @@
 	// apps manifests are handled by aapt, don't let Module see them
 	a.properties.Manifest = nil
 
+	a.linter.mergedManifest = a.aapt.mergedManifestFile
+	a.linter.manifest = a.aapt.manifestPath
+	a.linter.resources = a.aapt.resourceFiles
+
 	a.Module.extraProguardFlagFiles = append(a.Module.extraProguardFlagFiles,
 		a.proguardOptionsFile)
 
@@ -512,6 +518,7 @@
 		&module.androidLibraryProperties)
 
 	module.androidLibraryProperties.BuildAAR = true
+	module.Module.linter.library = true
 
 	android.InitApexModule(module)
 	InitJavaModule(module, android.DeviceSupported)
diff --git a/java/app.go b/java/app.go
index ab7ec57..445e531 100755
--- a/java/app.go
+++ b/java/app.go
@@ -742,6 +742,10 @@
 
 	a.proguardBuildActions(ctx)
 
+	a.linter.mergedManifest = a.aapt.mergedManifestFile
+	a.linter.manifest = a.aapt.manifestPath
+	a.linter.resources = a.aapt.resourceFiles
+
 	dexJarFile := a.dexBuildActions(ctx)
 
 	jniLibs, certificateDeps := collectAppDeps(ctx, a, a.shouldEmbedJnis(ctx), !Bool(a.appProperties.Jni_uses_platform_apis))
@@ -1079,6 +1083,7 @@
 	module.appProperties.Use_embedded_native_libs = proptools.BoolPtr(true)
 	module.appProperties.AlwaysPackageNativeLibs = true
 	module.Module.dexpreopter.isTest = true
+	module.Module.linter.test = true
 
 	module.addHostAndDeviceProperties()
 	module.AddProperties(
@@ -1128,6 +1133,7 @@
 	module.appProperties.Use_embedded_native_libs = proptools.BoolPtr(true)
 	module.appProperties.AlwaysPackageNativeLibs = true
 	module.Module.dexpreopter.isTest = true
+	module.Module.linter.test = true
 
 	module.addHostAndDeviceProperties()
 	module.AddProperties(
diff --git a/java/droiddoc.go b/java/droiddoc.go
index 68cfe9f..d6bff95 100644
--- a/java/droiddoc.go
+++ b/java/droiddoc.go
@@ -1224,6 +1224,21 @@
 	return module
 }
 
+func (d *Droidstubs) OutputFiles(tag string) (android.Paths, error) {
+	switch tag {
+	case "":
+		return android.Paths{d.stubsSrcJar}, nil
+	case ".docs.zip":
+		return android.Paths{d.docZip}, nil
+	case ".annotations.zip":
+		return android.Paths{d.annotationsZip}, nil
+	case ".api_versions.xml":
+		return android.Paths{d.apiVersionsXml}, nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
 func (d *Droidstubs) ApiFilePath() android.Path {
 	return d.apiFilePath
 }
diff --git a/java/java.go b/java/java.go
index b343584..6c14c78 100644
--- a/java/java.go
+++ b/java/java.go
@@ -471,6 +471,7 @@
 
 	hiddenAPI
 	dexpreopter
+	linter
 
 	// list of the xref extraction files
 	kytheFiles android.Paths
@@ -493,6 +494,7 @@
 	j.AddProperties(
 		&j.deviceProperties,
 		&j.dexpreoptProperties,
+		&j.linter.properties,
 	)
 }
 
@@ -1647,6 +1649,28 @@
 		outputFile = implementationAndResourcesJar
 	}
 
+	if ctx.Device() {
+		lintSDKVersionString := func(sdkSpec sdkSpec) string {
+			if v := sdkSpec.version; v.isNumbered() {
+				return v.String()
+			} else {
+				return ctx.Config().DefaultAppTargetSdk()
+			}
+		}
+
+		j.linter.name = ctx.ModuleName()
+		j.linter.srcs = srcFiles
+		j.linter.srcJars = srcJars
+		j.linter.classpath = append(append(android.Paths(nil), flags.bootClasspath...), flags.classpath...)
+		j.linter.classes = j.implementationJarFile
+		j.linter.minSdkVersion = lintSDKVersionString(j.minSdkVersion())
+		j.linter.targetSdkVersion = lintSDKVersionString(j.targetSdkVersion())
+		j.linter.compileSdkVersion = lintSDKVersionString(j.sdkVersion())
+		j.linter.javaLanguageLevel = flags.javaVersion.String()
+		j.linter.kotlinLanguageLevel = "1.3"
+		j.linter.lint(ctx)
+	}
+
 	ctx.CheckbuildFile(outputFile)
 
 	// Save the output file with no relative path so that it doesn't end up in a subdirectory when used as a resource
@@ -2255,6 +2279,7 @@
 
 	module.Module.properties.Installable = proptools.BoolPtr(true)
 	module.Module.dexpreopter.isTest = true
+	module.Module.linter.test = true
 
 	InitJavaModule(module, android.HostAndDeviceSupported)
 	return module
@@ -2269,6 +2294,7 @@
 
 	module.Module.properties.Installable = proptools.BoolPtr(true)
 	module.Module.dexpreopter.isTest = true
+	module.Module.linter.test = true
 
 	InitJavaModule(module, android.HostAndDeviceSupported)
 	return module
@@ -2856,6 +2882,7 @@
 		&DexImportProperties{},
 		&android.ApexProperties{},
 		&RuntimeResourceOverlayProperties{},
+		&LintProperties{},
 	)
 
 	android.InitDefaultsModule(module)
diff --git a/java/lint.go b/java/lint.go
new file mode 100644
index 0000000..441e110
--- /dev/null
+++ b/java/lint.go
@@ -0,0 +1,355 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// 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.
+
+package java
+
+import (
+	"fmt"
+	"sort"
+
+	"android/soong/android"
+)
+
+type LintProperties struct {
+	// Controls for running Android Lint on the module.
+	Lint struct {
+
+		// If true, run Android Lint on the module.  Defaults to true.
+		Enabled *bool
+
+		// Flags to pass to the Android Lint tool.
+		Flags []string
+
+		// Checks that should be treated as fatal.
+		Fatal_checks []string
+
+		// Checks that should be treated as errors.
+		Error_checks []string
+
+		// Checks that should be treated as warnings.
+		Warning_checks []string
+
+		// Checks that should be skipped.
+		Disabled_checks []string
+	}
+}
+
+type linter struct {
+	name                string
+	manifest            android.Path
+	mergedManifest      android.Path
+	srcs                android.Paths
+	srcJars             android.Paths
+	resources           android.Paths
+	classpath           android.Paths
+	classes             android.Path
+	extraLintCheckJars  android.Paths
+	test                bool
+	library             bool
+	minSdkVersion       string
+	targetSdkVersion    string
+	compileSdkVersion   string
+	javaLanguageLevel   string
+	kotlinLanguageLevel string
+	outputs             lintOutputs
+	properties          LintProperties
+}
+
+type lintOutputs struct {
+	html android.ModuleOutPath
+	text android.ModuleOutPath
+	xml  android.ModuleOutPath
+}
+
+func (l *linter) enabled() bool {
+	return BoolDefault(l.properties.Lint.Enabled, true)
+}
+
+func (l *linter) writeLintProjectXML(ctx android.ModuleContext,
+	rule *android.RuleBuilder) (projectXMLPath, configXMLPath, cacheDir android.WritablePath, deps android.Paths) {
+
+	var resourcesList android.WritablePath
+	if len(l.resources) > 0 {
+		// The list of resources may be too long to put on the command line, but
+		// we can't use the rsp file because it is already being used for srcs.
+		// Insert a second rule to write out the list of resources to a file.
+		resourcesList = android.PathForModuleOut(ctx, "lint", "resources.list")
+		resListRule := android.NewRuleBuilder()
+		resListRule.Command().Text("cp").FlagWithRspFileInputList("", l.resources).Output(resourcesList)
+		resListRule.Build(pctx, ctx, "lint_resources_list", "lint resources list")
+		deps = append(deps, l.resources...)
+	}
+
+	projectXMLPath = android.PathForModuleOut(ctx, "lint", "project.xml")
+	// Lint looks for a lint.xml file next to the project.xml file, give it one.
+	configXMLPath = android.PathForModuleOut(ctx, "lint", "lint.xml")
+	cacheDir = android.PathForModuleOut(ctx, "lint", "cache")
+
+	srcJarDir := android.PathForModuleOut(ctx, "lint-srcjars")
+	srcJarList := zipSyncCmd(ctx, rule, srcJarDir, l.srcJars)
+
+	cmd := rule.Command().
+		BuiltTool(ctx, "lint-project-xml").
+		FlagWithOutput("--project_out ", projectXMLPath).
+		FlagWithOutput("--config_out ", configXMLPath).
+		FlagWithArg("--name ", ctx.ModuleName())
+
+	if l.library {
+		cmd.Flag("--library")
+	}
+	if l.test {
+		cmd.Flag("--test")
+	}
+	if l.manifest != nil {
+		deps = append(deps, l.manifest)
+		cmd.FlagWithArg("--manifest ", l.manifest.String())
+	}
+	if l.mergedManifest != nil {
+		deps = append(deps, l.mergedManifest)
+		cmd.FlagWithArg("--merged_manifest ", l.mergedManifest.String())
+	}
+
+	// TODO(ccross): some of the files in l.srcs are generated sources and should be passed to
+	// lint separately.
+	cmd.FlagWithRspFileInputList("--srcs ", l.srcs)
+	deps = append(deps, l.srcs...)
+
+	cmd.FlagWithInput("--generated_srcs ", srcJarList)
+	deps = append(deps, l.srcJars...)
+
+	if resourcesList != nil {
+		cmd.FlagWithInput("--resources ", resourcesList)
+	}
+
+	if l.classes != nil {
+		deps = append(deps, l.classes)
+		cmd.FlagWithArg("--classes ", l.classes.String())
+	}
+
+	cmd.FlagForEachArg("--classpath ", l.classpath.Strings())
+	deps = append(deps, l.classpath...)
+
+	cmd.FlagForEachArg("--extra_checks_jar ", l.extraLintCheckJars.Strings())
+	deps = append(deps, l.extraLintCheckJars...)
+
+	// The cache tag in project.xml is relative to the project.xml file.
+	cmd.FlagWithArg("--cache_dir ", "cache")
+
+	cmd.FlagWithInput("@",
+		android.PathForSource(ctx, "build/soong/java/lint_defaults.txt"))
+
+	cmd.FlagForEachArg("--disable_check ", l.properties.Lint.Disabled_checks)
+	cmd.FlagForEachArg("--warning_check ", l.properties.Lint.Warning_checks)
+	cmd.FlagForEachArg("--error_check ", l.properties.Lint.Error_checks)
+	cmd.FlagForEachArg("--fatal_check ", l.properties.Lint.Fatal_checks)
+
+	return projectXMLPath, configXMLPath, cacheDir, deps
+}
+
+// generateManifest adds a command to the rule to write a dummy manifest cat contains the
+// minSdkVersion and targetSdkVersion for modules (like java_library) that don't have a manifest.
+func (l *linter) generateManifest(ctx android.ModuleContext, rule *android.RuleBuilder) android.Path {
+	manifestPath := android.PathForModuleOut(ctx, "lint", "AndroidManifest.xml")
+
+	rule.Command().Text("(").
+		Text(`echo "<?xml version='1.0' encoding='utf-8'?>" &&`).
+		Text(`echo "<manifest xmlns:android='http://schemas.android.com/apk/res/android'" &&`).
+		Text(`echo "    android:versionCode='1' android:versionName='1' >" &&`).
+		Textf(`echo "  <uses-sdk android:minSdkVersion='%s' android:targetSdkVersion='%s'/>" &&`,
+			l.minSdkVersion, l.targetSdkVersion).
+		Text(`echo "</manifest>"`).
+		Text(") >").Output(manifestPath)
+
+	return manifestPath
+}
+
+func (l *linter) lint(ctx android.ModuleContext) {
+	if !l.enabled() {
+		return
+	}
+
+	rule := android.NewRuleBuilder()
+
+	if l.manifest == nil {
+		manifest := l.generateManifest(ctx, rule)
+		l.manifest = manifest
+	}
+
+	projectXML, lintXML, cacheDir, deps := l.writeLintProjectXML(ctx, rule)
+
+	l.outputs.html = android.PathForModuleOut(ctx, "lint-report.html")
+	l.outputs.text = android.PathForModuleOut(ctx, "lint-report.txt")
+	l.outputs.xml = android.PathForModuleOut(ctx, "lint-report.xml")
+
+	rule.Command().Text("rm -rf").Flag(cacheDir.String())
+	rule.Command().Text("mkdir -p").Flag(cacheDir.String())
+
+	rule.Command().
+		Text("(").
+		Flag("JAVA_OPTS=-Xmx2048m").
+		FlagWithInput("SDK_ANNOTATIONS=", annotationsZipPath(ctx)).
+		FlagWithInput("LINT_OPTS=-DLINT_API_DATABASE=", apiVersionsXmlPath(ctx)).
+		Tool(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/bin/lint")).
+		Implicit(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/lib/lint-classpath.jar")).
+		Flag("--quiet").
+		FlagWithInput("--project ", projectXML).
+		FlagWithInput("--config ", lintXML).
+		FlagWithOutput("--html ", l.outputs.html).
+		FlagWithOutput("--text ", l.outputs.text).
+		FlagWithOutput("--xml ", l.outputs.xml).
+		FlagWithArg("--compile-sdk-version ", l.compileSdkVersion).
+		FlagWithArg("--java-language-level ", l.javaLanguageLevel).
+		FlagWithArg("--kotlin-language-level ", l.kotlinLanguageLevel).
+		FlagWithArg("--url ", fmt.Sprintf(".=.,%s=out", android.PathForOutput(ctx).String())).
+		Flag("--exitcode").
+		Flags(l.properties.Lint.Flags).
+		Implicits(deps).
+		Text("|| (").Text("cat").Input(l.outputs.text).Text("; exit 7)").
+		Text(")")
+
+	rule.Command().Text("rm -rf").Flag(cacheDir.String())
+
+	rule.Build(pctx, ctx, "lint", "lint")
+}
+
+func (l *linter) lintOutputs() *lintOutputs {
+	return &l.outputs
+}
+
+type lintOutputIntf interface {
+	lintOutputs() *lintOutputs
+}
+
+var _ lintOutputIntf = (*linter)(nil)
+
+type lintSingleton struct {
+	htmlZip android.WritablePath
+	textZip android.WritablePath
+	xmlZip  android.WritablePath
+}
+
+func (l *lintSingleton) GenerateBuildActions(ctx android.SingletonContext) {
+	l.generateLintReportZips(ctx)
+	l.copyLintDependencies(ctx)
+}
+
+func (l *lintSingleton) copyLintDependencies(ctx android.SingletonContext) {
+	if ctx.Config().UnbundledBuild() {
+		return
+	}
+
+	var frameworkDocStubs android.Module
+	ctx.VisitAllModules(func(m android.Module) {
+		if ctx.ModuleName(m) == "framework-doc-stubs" {
+			if frameworkDocStubs == nil {
+				frameworkDocStubs = m
+			} else {
+				ctx.Errorf("lint: multiple framework-doc-stubs modules found: %s and %s",
+					ctx.ModuleSubDir(m), ctx.ModuleSubDir(frameworkDocStubs))
+			}
+		}
+	})
+
+	if frameworkDocStubs == nil {
+		if !ctx.Config().AllowMissingDependencies() {
+			ctx.Errorf("lint: missing framework-doc-stubs")
+		}
+		return
+	}
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   android.Cp,
+		Input:  android.OutputFileForModule(ctx, frameworkDocStubs, ".annotations.zip"),
+		Output: annotationsZipPath(ctx),
+	})
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   android.Cp,
+		Input:  android.OutputFileForModule(ctx, frameworkDocStubs, ".api_versions.xml"),
+		Output: apiVersionsXmlPath(ctx),
+	})
+}
+
+func annotationsZipPath(ctx android.PathContext) android.WritablePath {
+	return android.PathForOutput(ctx, "lint", "annotations.zip")
+}
+
+func apiVersionsXmlPath(ctx android.PathContext) android.WritablePath {
+	return android.PathForOutput(ctx, "lint", "api_versions.xml")
+}
+
+func (l *lintSingleton) generateLintReportZips(ctx android.SingletonContext) {
+	var outputs []*lintOutputs
+	var dirs []string
+	ctx.VisitAllModules(func(m android.Module) {
+		if ctx.Config().EmbeddedInMake() && !m.ExportedToMake() {
+			return
+		}
+
+		if apex, ok := m.(android.ApexModule); ok && apex.NotAvailableForPlatform() && apex.IsForPlatform() {
+			// There are stray platform variants of modules in apexes that are not available for
+			// the platform, and they sometimes can't be built.  Don't depend on them.
+			return
+		}
+
+		if l, ok := m.(lintOutputIntf); ok {
+			outputs = append(outputs, l.lintOutputs())
+		}
+	})
+
+	dirs = android.SortedUniqueStrings(dirs)
+
+	zip := func(outputPath android.WritablePath, get func(*lintOutputs) android.Path) {
+		var paths android.Paths
+
+		for _, output := range outputs {
+			paths = append(paths, get(output))
+		}
+
+		sort.Slice(paths, func(i, j int) bool {
+			return paths[i].String() < paths[j].String()
+		})
+
+		rule := android.NewRuleBuilder()
+
+		rule.Command().BuiltTool(ctx, "soong_zip").
+			FlagWithOutput("-o ", outputPath).
+			FlagWithArg("-C ", android.PathForIntermediates(ctx).String()).
+			FlagWithRspFileInputList("-l ", paths)
+
+		rule.Build(pctx, ctx, outputPath.Base(), outputPath.Base())
+	}
+
+	l.htmlZip = android.PathForOutput(ctx, "lint-report-html.zip")
+	zip(l.htmlZip, func(l *lintOutputs) android.Path { return l.html })
+
+	l.textZip = android.PathForOutput(ctx, "lint-report-text.zip")
+	zip(l.textZip, func(l *lintOutputs) android.Path { return l.text })
+
+	l.xmlZip = android.PathForOutput(ctx, "lint-report-xml.zip")
+	zip(l.xmlZip, func(l *lintOutputs) android.Path { return l.xml })
+
+	ctx.Phony("lint-check", l.htmlZip, l.textZip, l.xmlZip)
+}
+
+func (l *lintSingleton) MakeVars(ctx android.MakeVarsContext) {
+	ctx.DistForGoal("lint-check", l.htmlZip, l.textZip, l.xmlZip)
+}
+
+var _ android.SingletonMakeVarsProvider = (*lintSingleton)(nil)
+
+func init() {
+	android.RegisterSingletonType("lint",
+		func() android.Singleton { return &lintSingleton{} })
+}
diff --git a/java/lint_defaults.txt b/java/lint_defaults.txt
new file mode 100644
index 0000000..0786b7c
--- /dev/null
+++ b/java/lint_defaults.txt
@@ -0,0 +1,78 @@
+# Treat LintError as fatal to catch invocation errors
+--fatal_check LintError
+
+# Downgrade existing errors to warnings
+--warning_check AppCompatResource                  # 55 occurences in 10 modules
+--warning_check AppLinkUrlError                    # 111 occurences in 53 modules
+--warning_check BlockedPrivateApi                  # 2 occurences in 2 modules
+--warning_check ByteOrderMark                      # 2 occurences in 2 modules
+--warning_check DuplicateActivity                  # 3 occurences in 3 modules
+--warning_check DuplicateDefinition                # 3623 occurences in 48 modules
+--warning_check DuplicateIds                       # 207 occurences in 22 modules
+--warning_check EllipsizeMaxLines                  # 12 occurences in 7 modules
+--warning_check ExtraTranslation                   # 21276 occurences in 27 modules
+--warning_check FontValidationError                # 4 occurences in 1 modules
+--warning_check FullBackupContent                  # 16 occurences in 1 modules
+--warning_check GetContentDescriptionOverride      # 3 occurences in 2 modules
+--warning_check HalfFloat                          # 31 occurences in 1 modules
+--warning_check HardcodedDebugMode                 # 99 occurences in 95 modules
+--warning_check ImpliedQuantity                    # 703 occurences in 27 modules
+--warning_check ImpliedTouchscreenHardware         # 4 occurences in 4 modules
+--warning_check IncludeLayoutParam                 # 11 occurences in 6 modules
+--warning_check Instantiatable                     # 145 occurences in 19 modules
+--warning_check InvalidPermission                  # 6 occurences in 4 modules
+--warning_check InvalidUsesTagAttribute            # 6 occurences in 2 modules
+--warning_check InvalidWakeLockTag                 # 111 occurences in 37 modules
+--warning_check JavascriptInterface                # 3 occurences in 2 modules
+--warning_check LibraryCustomView                  # 9 occurences in 4 modules
+--warning_check LogTagMismatch                     # 81 occurences in 13 modules
+--warning_check LongLogTag                         # 249 occurences in 12 modules
+--warning_check MenuTitle                          # 5 occurences in 4 modules
+--warning_check MissingClass                       # 537 occurences in 141 modules
+--warning_check MissingConstraints                 # 39 occurences in 10 modules
+--warning_check MissingDefaultResource             # 1257 occurences in 40 modules
+--warning_check MissingIntentFilterForMediaSearch  # 1 occurences in 1 modules
+--warning_check MissingLeanbackLauncher            # 3 occurences in 3 modules
+--warning_check MissingLeanbackSupport             # 2 occurences in 2 modules
+--warning_check MissingOnPlayFromSearch            # 1 occurences in 1 modules
+--warning_check MissingPermission                  # 2071 occurences in 150 modules
+--warning_check MissingPrefix                      # 46 occurences in 41 modules
+--warning_check MissingQuantity                    # 100 occurences in 1 modules
+--warning_check MissingSuperCall                   # 121 occurences in 36 modules
+--warning_check MissingTvBanner                    # 3 occurences in 3 modules
+--warning_check NamespaceTypo                      # 3 occurences in 3 modules
+--warning_check NetworkSecurityConfig              # 46 occurences in 12 modules
+--warning_check NewApi                             # 1996 occurences in 122 modules
+--warning_check NotSibling                         # 15 occurences in 10 modules
+--warning_check ObjectAnimatorBinding              # 14 occurences in 5 modules
+--warning_check OnClick                            # 49 occurences in 21 modules
+--warning_check Orientation                        # 77 occurences in 19 modules
+--warning_check Override                           # 385 occurences in 36 modules
+--warning_check ParcelCreator                      # 23 occurences in 2 modules
+--warning_check ProtectedPermissions               # 2413 occurences in 381 modules
+--warning_check Range                              # 80 occurences in 28 modules
+--warning_check RecyclerView                       # 1 occurences in 1 modules
+--warning_check ReferenceType                      # 4 occurences in 1 modules
+--warning_check ResourceAsColor                    # 19 occurences in 14 modules
+--warning_check RequiredSize                       # 52 occurences in 13 modules
+--warning_check ResAuto                            # 3 occurences in 1 modules
+--warning_check ResourceCycle                      # 37 occurences in 10 modules
+--warning_check ResourceType                       # 137 occurences in 36 modules
+--warning_check RestrictedApi                      # 28 occurences in 5 modules
+--warning_check RtlCompat                          # 9 occurences in 6 modules
+--warning_check ServiceCast                        # 3 occurences in 1 modules
+--warning_check SoonBlockedPrivateApi              # 5 occurences in 3 modules
+--warning_check StringFormatInvalid                # 148 occurences in 11 modules
+--warning_check StringFormatMatches                # 4800 occurences in 30 modules
+--warning_check UnknownId                          # 8 occurences in 7 modules
+--warning_check ValidFragment                      # 12 occurences in 5 modules
+--warning_check ValidRestrictions                  # 5 occurences in 1 modules
+--warning_check WebViewLayout                      # 3 occurences in 1 modules
+--warning_check WrongCall                          # 21 occurences in 3 modules
+--warning_check WrongConstant                      # 894 occurences in 126 modules
+--warning_check WrongManifestParent                # 10 occurences in 4 modules
+--warning_check WrongThread                        # 14 occurences in 6 modules
+--warning_check WrongViewCast                      # 1 occurences in 1 modules
+
+# TODO(b/158390965): remove this when lint doesn't crash
+--disable_check HardcodedDebugMode
diff --git a/java/robolectric.go b/java/robolectric.go
index 64bd325..c6b07a1 100644
--- a/java/robolectric.go
+++ b/java/robolectric.go
@@ -221,6 +221,7 @@
 		&module.robolectricProperties)
 
 	module.Module.dexpreopter.isTest = true
+	module.Module.linter.test = true
 
 	InitJavaModule(module, android.DeviceSupported)
 	return module
diff --git a/java/sdk_library.go b/java/sdk_library.go
index 83bc04e..4b1c3e5 100644
--- a/java/sdk_library.go
+++ b/java/sdk_library.go
@@ -1090,6 +1090,7 @@
 		&module.protoProperties,
 		&module.deviceProperties,
 		&module.dexpreoptProperties,
+		&module.linter.properties,
 		&props,
 		module.sdkComponentPropertiesForChildLibrary(),
 	}
diff --git a/scripts/Android.bp b/scripts/Android.bp
index e848b50..1f55030 100644
--- a/scripts/Android.bp
+++ b/scripts/Android.bp
@@ -148,3 +148,9 @@
     ],
     test_suites: ["general-tests"],
 }
+
+python_binary_host {
+    name: "lint-project-xml",
+    main: "lint-project-xml.py",
+    srcs: ["lint-project-xml.py"],
+}
diff --git a/scripts/lint-project-xml.py b/scripts/lint-project-xml.py
new file mode 100755
index 0000000..7ab4f01
--- /dev/null
+++ b/scripts/lint-project-xml.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+#
+
+"""This file generates project.xml and lint.xml files used to drive the Android Lint CLI tool."""
+
+import argparse
+
+
+def check_action(check_type):
+  """
+  Returns an action that appends a tuple of check_type and the argument to the dest.
+  """
+  class CheckAction(argparse.Action):
+    def __init__(self, option_strings, dest, nargs=None, **kwargs):
+      if nargs is not None:
+        raise ValueError("nargs must be None, was %s" % nargs)
+      super(CheckAction, self).__init__(option_strings, dest, **kwargs)
+    def __call__(self, parser, namespace, values, option_string=None):
+      checks = getattr(namespace, self.dest, [])
+      checks.append((check_type, values))
+      setattr(namespace, self.dest, checks)
+  return CheckAction
+
+
+def parse_args():
+  """Parse commandline arguments."""
+
+  def convert_arg_line_to_args(arg_line):
+    for arg in arg_line.split():
+      if arg.startswith('#'):
+        return
+      if not arg.strip():
+        continue
+      yield arg
+
+  parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+  parser.convert_arg_line_to_args = convert_arg_line_to_args
+  parser.add_argument('--project_out', dest='project_out',
+                      help='file to which the project.xml contents will be written.')
+  parser.add_argument('--config_out', dest='config_out',
+                      help='file to which the lint.xml contents will be written.')
+  parser.add_argument('--name', dest='name',
+                      help='name of the module.')
+  parser.add_argument('--srcs', dest='srcs', action='append', default=[],
+                      help='file containing whitespace separated list of source files.')
+  parser.add_argument('--generated_srcs', dest='generated_srcs', action='append', default=[],
+                      help='file containing whitespace separated list of generated source files.')
+  parser.add_argument('--resources', dest='resources', action='append', default=[],
+                      help='file containing whitespace separated list of resource files.')
+  parser.add_argument('--classes', dest='classes', action='append', default=[],
+                      help='file containing the module\'s classes.')
+  parser.add_argument('--classpath', dest='classpath', action='append', default=[],
+                      help='file containing classes from dependencies.')
+  parser.add_argument('--extra_checks_jar', dest='extra_checks_jars', action='append', default=[],
+                      help='file containing extra lint checks.')
+  parser.add_argument('--manifest', dest='manifest',
+                      help='file containing the module\'s manifest.')
+  parser.add_argument('--merged_manifest', dest='merged_manifest',
+                      help='file containing merged manifest for the module and its dependencies.')
+  parser.add_argument('--library', dest='library', action='store_true',
+                      help='mark the module as a library.')
+  parser.add_argument('--test', dest='test', action='store_true',
+                      help='mark the module as a test.')
+  parser.add_argument('--cache_dir', dest='cache_dir',
+                      help='directory to use for cached file.')
+  group = parser.add_argument_group('check arguments', 'later arguments override earlier ones.')
+  group.add_argument('--fatal_check', dest='checks', action=check_action('fatal'), default=[],
+                     help='treat a lint issue as a fatal error.')
+  group.add_argument('--error_check', dest='checks', action=check_action('error'), default=[],
+                     help='treat a lint issue as an error.')
+  group.add_argument('--warning_check', dest='checks', action=check_action('warning'), default=[],
+                     help='treat a lint issue as a warning.')
+  group.add_argument('--disable_check', dest='checks', action=check_action('ignore'), default=[],
+                     help='disable a lint issue.')
+  return parser.parse_args()
+
+
+class NinjaRspFileReader:
+  """
+  Reads entries from a Ninja rsp file.  Ninja escapes any entries in the file that contain a
+  non-standard character by surrounding the whole entry with single quotes, and then replacing
+  any single quotes in the entry with the escape sequence '\''.
+  """
+
+  def __init__(self, filename):
+    self.f = open(filename, 'r')
+    self.r = self.character_reader(self.f)
+
+  def __iter__(self):
+    return self
+
+  def character_reader(self, f):
+    """Turns a file into a generator that returns one character at a time."""
+    while True:
+      c = f.read(1)
+      if c:
+        yield c
+      else:
+        return
+
+  def __next__(self):
+    entry = self.read_entry()
+    if entry:
+      return entry
+    else:
+      raise StopIteration
+
+  def read_entry(self):
+    c = next(self.r, "")
+    if not c:
+      return ""
+    elif c == "'":
+      return self.read_quoted_entry()
+    else:
+      entry = c
+      for c in self.r:
+        if c == " " or c == "\n":
+          break
+        entry += c
+      return entry
+
+  def read_quoted_entry(self):
+    entry = ""
+    for c in self.r:
+      if c == "'":
+        # Either the end of the quoted entry, or the beginning of an escape sequence, read the next
+        # character to find out.
+        c = next(self.r)
+        if not c or c == " " or c == "\n":
+          # End of the item
+          return entry
+        elif c == "\\":
+          # Escape sequence, expect a '
+          c = next(self.r)
+          if c != "'":
+            # Malformed escape sequence
+            raise "malformed escape sequence %s'\\%s" % (entry, c)
+          entry += "'"
+        else:
+          raise "malformed escape sequence %s'%s" % (entry, c)
+      else:
+        entry += c
+    raise "unterminated quoted entry %s" % entry
+
+
+def write_project_xml(f, args):
+  test_attr = "test='true' " if args.test else ""
+
+  f.write("<?xml version='1.0' encoding='utf-8'?>\n")
+  f.write("<project>\n")
+  f.write("  <module name='%s' android='true' %sdesugar='full' >\n" % (args.name, "library='true' " if args.library else ""))
+  if args.manifest:
+    f.write("    <manifest file='%s' %s/>\n" % (args.manifest, test_attr))
+  if args.merged_manifest:
+    f.write("    <merged-manifest file='%s' %s/>\n" % (args.merged_manifest, test_attr))
+  for src_file in args.srcs:
+    for src in NinjaRspFileReader(src_file):
+      f.write("    <src file='%s' %s/>\n" % (src, test_attr))
+  for src_file in args.generated_srcs:
+    for src in NinjaRspFileReader(src_file):
+      f.write("    <src file='%s' generated='true' %s/>\n" % (src, test_attr))
+  for res_file in args.resources:
+    for res in NinjaRspFileReader(res_file):
+      f.write("    <resource file='%s' %s/>\n" % (res, test_attr))
+  for classes in args.classes:
+    f.write("    <classes jar='%s' />\n" % classes)
+  for classpath in args.classpath:
+    f.write("    <classpath jar='%s' />\n" % classpath)
+  for extra in args.extra_checks_jars:
+    f.write("    <lint-checks jar='%s' />\n" % extra)
+  f.write("  </module>\n")
+  if args.cache_dir:
+    f.write("  <cache dir='%s'/>\n" % args.cache_dir)
+  f.write("</project>\n")
+
+
+def write_config_xml(f, args):
+  f.write("<?xml version='1.0' encoding='utf-8'?>\n")
+  f.write("<lint>\n")
+  for check in args.checks:
+    f.write("  <issue id='%s' severity='%s' />\n" % (check[1], check[0]))
+  f.write("</lint>\n")
+
+
+def main():
+  """Program entry point."""
+  args = parse_args()
+
+  if args.project_out:
+    with open(args.project_out, 'w') as f:
+      write_project_xml(f, args)
+
+  if args.config_out:
+    with open(args.config_out, 'w') as f:
+      write_config_xml(f, args)
+
+
+if __name__ == '__main__':
+  main()