Add clippy-driver build rule

Depending on the location of the repository (e.g. external/, vendor/), a
different set of lints will be enabled. Add the clippy property to the
rust_* modules. This property can be used to overwrite the default
behaviour.

Test: m checkbuild
Bug: 157238651
Change-Id: Ife0f723ef4a74abb102597f8486a7b9f30e7d351
diff --git a/rust/Android.bp b/rust/Android.bp
index b06ea8e..d56de87 100644
--- a/rust/Android.bp
+++ b/rust/Android.bp
@@ -9,24 +9,26 @@
     ],
     srcs: [
         "androidmk.go",
-        "compiler.go",
-        "coverage.go",
         "binary.go",
         "builder.go",
+        "clippy.go",
+        "compiler.go",
+        "coverage.go",
         "library.go",
         "prebuilt.go",
         "proc_macro.go",
-	"project_json.go",
+        "project_json.go",
         "rust.go",
         "test.go",
         "testing.go",
     ],
     testSrcs: [
         "binary_test.go",
+        "clippy_test.go",
         "compiler_test.go",
         "coverage_test.go",
         "library_test.go",
-	"project_json_test.go",
+        "project_json_test.go",
         "rust_test.go",
         "test_test.go",
     ],
diff --git a/rust/builder.go b/rust/builder.go
index 7dbb59d..b191323 100644
--- a/rust/builder.go
+++ b/rust/builder.go
@@ -39,6 +39,18 @@
 		},
 		"rustcFlags", "linkFlags", "libFlags", "crtBegin", "crtEnd")
 
+	_            = pctx.SourcePathVariable("clippyCmd", "${config.RustBin}/clippy-driver")
+	clippyDriver = pctx.AndroidStaticRule("clippy",
+		blueprint.RuleParams{
+			Command: "$clippyCmd " +
+				// Because clippy-driver uses rustc as backend, we need to have some output even during the linting.
+				// Use the metadata output as it has the smallest footprint.
+				"--emit metadata -o $out $in ${libFlags} " +
+				"$clippyFlags $rustcFlags",
+			CommandDeps: []string{"$clippyCmd"},
+		},
+		"rustcFlags", "libFlags", "clippyFlags")
+
 	zip = pctx.AndroidStaticRule("zip",
 		blueprint.RuleParams{
 			Command:        "cat $out.rsp | tr ' ' '\\n' | tr -d \\' | sort -u > ${out}.tmp && ${SoongZipCmd} -o ${out} -C $$OUT_DIR -l ${out}.tmp",
@@ -125,10 +137,14 @@
 		rustcFlags = append(rustcFlags, "--target="+targetTriple)
 		linkFlags = append(linkFlags, "-target "+targetTriple)
 	}
-	// TODO once we have static libraries in the host prebuilt .bp, this
-	// should be unconditionally added.
-	if !(ctx.Host() && ctx.TargetPrimary()) {
-		// If we're not targeting the host primary arch, do not use an implicit sysroot
+	// TODO(b/159718669): Once we have defined static libraries in the host
+	// prebuilts Blueprint file, sysroot should be unconditionally sourced
+	// from /dev/null. Explicitly set sysroot to avoid clippy-driver to
+	// internally call rustc.
+	if ctx.Host() && ctx.TargetPrimary() {
+		rustcFlags = append(rustcFlags, "--sysroot=${config.RustPath}")
+	} else {
+		// If we're not targeting the host primary arch, do not use a sysroot.
 		rustcFlags = append(rustcFlags, "--sysroot=/dev/null")
 	}
 	// Collect linker flags
@@ -179,6 +195,25 @@
 		output.coverageFile = gcnoFile
 	}
 
+	if flags.Clippy {
+		clippyFile := android.PathForModuleOut(ctx, outputFile.Base()+".clippy")
+		ctx.Build(pctx, android.BuildParams{
+			Rule:            clippyDriver,
+			Description:     "clippy " + main.Rel(),
+			Output:          clippyFile,
+			ImplicitOutputs: nil,
+			Inputs:          inputs,
+			Implicits:       implicits,
+			Args: map[string]string{
+				"rustcFlags":  strings.Join(rustcFlags, " "),
+				"libFlags":    strings.Join(libFlags, " "),
+				"clippyFlags": strings.Join(flags.ClippyFlags, " "),
+			},
+		})
+		// Declare the clippy build as an implicit dependency of the original crate.
+		implicits = append(implicits, clippyFile)
+	}
+
 	ctx.Build(pctx, android.BuildParams{
 		Rule:            rustc,
 		Description:     "rustc " + main.Rel(),
diff --git a/rust/clippy.go b/rust/clippy.go
new file mode 100644
index 0000000..e1f567d
--- /dev/null
+++ b/rust/clippy.go
@@ -0,0 +1,42 @@
+// Copyright 2020 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.
+
+package rust
+
+import (
+	"android/soong/rust/config"
+)
+
+type ClippyProperties struct {
+	// whether to run clippy.
+	Clippy *bool
+}
+
+type clippy struct {
+	Properties ClippyProperties
+}
+
+func (c *clippy) props() []interface{} {
+	return []interface{}{&c.Properties}
+}
+
+func (c *clippy) flags(ctx ModuleContext, flags Flags, deps PathDeps) (Flags, PathDeps) {
+	if c.Properties.Clippy != nil && !*c.Properties.Clippy {
+		return flags, deps
+	}
+	enabled, lints := config.ClippyLintsForDir(ctx.ModuleDir())
+	flags.Clippy = enabled
+	flags.ClippyFlags = append(flags.ClippyFlags, lints)
+	return flags, deps
+}
diff --git a/rust/clippy_test.go b/rust/clippy_test.go
new file mode 100644
index 0000000..af5cd17
--- /dev/null
+++ b/rust/clippy_test.go
@@ -0,0 +1,46 @@
+// Copyright 2020 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.
+
+package rust
+
+import (
+	"testing"
+)
+
+func TestClippy(t *testing.T) {
+	ctx := testRust(t, `
+		rust_library {
+			name: "libfoo",
+			srcs: ["foo.rs"],
+			crate_name: "foo",
+		}
+		rust_library {
+			name: "libfoobar",
+			srcs: ["foo.rs"],
+			crate_name: "foobar",
+			clippy: false,
+		}`)
+
+	ctx.ModuleForTests("libfoo", "android_arm64_armv8-a_shared").Output("libfoo.so")
+	fooClippy := ctx.ModuleForTests("libfoo", "android_arm64_armv8-a_shared").MaybeRule("clippy")
+	if fooClippy.Rule.String() != "android/soong/rust.clippy" {
+		t.Errorf("Clippy output (default) for libfoo was not generated: %+v", fooClippy)
+	}
+
+	ctx.ModuleForTests("libfoobar", "android_arm64_armv8-a_shared").Output("libfoobar.so")
+	foobarClippy := ctx.ModuleForTests("libfoobar", "android_arm64_armv8-a_shared").MaybeRule("clippy")
+	if foobarClippy.Rule != nil {
+		t.Errorf("Clippy output for libfoobar is not empty")
+	}
+}
diff --git a/rust/config/Android.bp b/rust/config/Android.bp
index 5026da3..1d30f82 100644
--- a/rust/config/Android.bp
+++ b/rust/config/Android.bp
@@ -9,6 +9,7 @@
         "arm_device.go",
         "arm64_device.go",
         "global.go",
+        "clippy.go",
         "toolchain.go",
         "allowed_list.go",
         "x86_darwin_host.go",
diff --git a/rust/config/clippy.go b/rust/config/clippy.go
new file mode 100644
index 0000000..c199ff2
--- /dev/null
+++ b/rust/config/clippy.go
@@ -0,0 +1,80 @@
+// Copyright 2020 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.
+
+package config
+
+import (
+	"strings"
+
+	"android/soong/android"
+)
+
+var (
+	defaultLints = []string{
+		"-D missing-docs",
+		"-D clippy::missing-safety-doc",
+	}
+	defaultVendorLints = []string{
+		"",
+	}
+)
+
+func init() {
+	// Default Rust lints. These apply to all Google-authored modules.
+	pctx.VariableFunc("ClippyDefaultLints", func(ctx android.PackageVarContext) string {
+		if override := ctx.Config().Getenv("CLIPPY_DEFAULT_LINTS"); override != "" {
+			return override
+		}
+		return strings.Join(defaultLints, " ")
+	})
+
+	// Rust lints that only applies to external code.
+	pctx.VariableFunc("ClippyVendorLints", func(ctx android.PackageVarContext) string {
+		if override := ctx.Config().Getenv("CLIPPY_VENDOR_LINTS"); override != "" {
+			return override
+		}
+		return strings.Join(defaultVendorLints, " ")
+	})
+}
+
+type PathBasedClippyConfig struct {
+	PathPrefix   string
+	Enabled      bool
+	ClippyConfig string
+}
+
+const clippyNone = ""
+const clippyDefault = "${config.ClippyDefaultLints}"
+const clippyVendor = "${config.ClippyVendorLints}"
+
+// This is a map of local path prefixes to a boolean indicating if the lint
+// rule should be generated and if so, the set of lints to use. The first entry
+// matching will be used. If no entry is matching, clippyDefault will be used.
+var DefaultLocalTidyChecks = []PathBasedClippyConfig{
+	{"external/", false, clippyNone},
+	{"hardware/", true, clippyVendor},
+	{"prebuilts/", false, clippyNone},
+	{"vendor/google", true, clippyDefault},
+	{"vendor/", true, clippyVendor},
+}
+
+// ClippyLintsForDir returns the Clippy lints to be used for a repository.
+func ClippyLintsForDir(dir string) (bool, string) {
+	for _, pathCheck := range DefaultLocalTidyChecks {
+		if strings.HasPrefix(dir, pathCheck.PathPrefix) {
+			return pathCheck.Enabled, pathCheck.ClippyConfig
+		}
+	}
+	return true, clippyDefault
+}
diff --git a/rust/rust.go b/rust/rust.go
index 7b82b1f..7f82873 100644
--- a/rust/rust.go
+++ b/rust/rust.go
@@ -49,8 +49,10 @@
 	GlobalLinkFlags []string // Flags that apply globally to linker
 	RustFlags       []string // Flags that apply to rust
 	LinkFlags       []string // Flags that apply to linker
+	ClippyFlags     []string // Flags that apply to clippy-driver, during the linting
 	Toolchain       config.Toolchain
 	Coverage        bool
+	Clippy          bool
 }
 
 type BaseProperties struct {
@@ -75,6 +77,7 @@
 
 	compiler         compiler
 	coverage         *coverage
+	clippy           *clippy
 	cachedToolchain  config.Toolchain
 	subAndroidMkOnce map[subAndroidMkProvider]bool
 	outputFile       android.OptionalPath
@@ -306,6 +309,7 @@
 		&PrebuiltProperties{},
 		&TestProperties{},
 		&cc.CoverageProperties{},
+		&ClippyProperties{},
 	)
 
 	android.InitDefaultsModule(module)
@@ -456,6 +460,9 @@
 	if mod.coverage != nil {
 		mod.AddProperties(mod.coverage.props()...)
 	}
+	if mod.clippy != nil {
+		mod.AddProperties(mod.clippy.props()...)
+	}
 
 	android.InitAndroidArchModule(mod, mod.hod, mod.multilib)
 
@@ -487,6 +494,7 @@
 func newModule(hod android.HostOrDeviceSupported, multilib android.Multilib) *Module {
 	module := newBaseModule(hod, multilib)
 	module.coverage = &coverage{}
+	module.clippy = &clippy{}
 	return module
 }
 
@@ -576,6 +584,9 @@
 	if mod.coverage != nil {
 		flags, deps = mod.coverage.flags(ctx, flags, deps)
 	}
+	if mod.clippy != nil {
+		flags, deps = mod.clippy.flags(ctx, flags, deps)
+	}
 
 	if mod.compiler != nil {
 		outputFile := mod.compiler.compile(ctx, flags, deps)