Make `path-mapping` messages specific and fix simple cases automatically

Each kind of path string (`File.path`, `File.dirname`, `File.root.path`,
`ctx.bin_dir.path`) now gets its own message tailored to where it is used.

Simple cases are fixed automatically: a `File.path` passed to `args.add()`,
`args.add_all()`, `args.add_joined()` or `executable`, optionally formatted
into a string literal or wrapped in a list comprehension, is replaced by the
`File` object and a `format` or `format_each` parameter. Variables created
with `ctx.actions.declare_directory()` are passed with
`expand_directories = False` since `args.add()` rejects directories.
diff --git a/WARNINGS.md b/WARNINGS.md
index 989440a..82cbc65 100644
--- a/WARNINGS.md
+++ b/WARNINGS.md
@@ -1204,7 +1204,7 @@
 ## <a name="path-mapping"></a>Action command lines should not contain path strings
 
   * Category name: `path-mapping`
-  * Automatic fix: no
+  * Automatic fix: yes
   * [Disabled by default](buildifier/README.md#linter)
   * [Suppress the warning](#suppress): `# buildifier: disable=path-mapping`
 
@@ -1227,6 +1227,8 @@
 ```python
 args = ctx.actions.args()
 args.add(src.path)
+args.add("--include=" + hdr.path)
+args.add_all([src.path for src in srcs])
 args.add(dir.path)
 args.add(src.dirname)
 ctx.actions.run_shell(
@@ -1244,6 +1246,8 @@
 
 args = ctx.actions.args()
 args.add(src)
+args.add(hdr, format = "--include=%s")
+args.add_all(srcs)
 args.add_all([dir], expand_directories = False)
 args.add_all([src], map_each = _dirname)
 ctx.actions.run(
@@ -1253,6 +1257,14 @@
 )
 ```
 
+Simple cases are fixed automatically: a `File.path` passed to `args.add()`,
+`args.add_all()`, `args.add_joined()` or `executable`, optionally formatted
+into a string literal or wrapped in a list comprehension, is replaced by the
+`File` object. Variables assigned from `ctx.actions.declare_directory()` are
+passed with `expand_directories = False`. Other directories have to be fixed
+manually, since `args.add()` rejects them and `args.add_all()` expands them by
+default.
+
 --------------------------------------------------------------------------------
 
 ## <a name="positional-args"></a>Keyword arguments should be used over positional arguments
diff --git a/warn/docs/warnings.textproto b/warn/docs/warnings.textproto
index 8c8ec28..7b57841 100644
--- a/warn/docs/warnings.textproto
+++ b/warn/docs/warnings.textproto
@@ -885,6 +885,8 @@
     "```python\n"
     "args = ctx.actions.args()\n"
     "args.add(src.path)\n"
+    "args.add(\"--include=\" + hdr.path)\n"
+    "args.add_all([src.path for src in srcs])\n"
     "args.add(dir.path)\n"
     "args.add(src.dirname)\n"
     "ctx.actions.run_shell(\n"
@@ -899,6 +901,8 @@
     "    return file.dirname\n\n"
     "args = ctx.actions.args()\n"
     "args.add(src)\n"
+    "args.add(hdr, format = \"--include=%s\")\n"
+    "args.add_all(srcs)\n"
     "args.add_all([dir], expand_directories = False)\n"
     "args.add_all([src], map_each = _dirname)\n"
     "ctx.actions.run(\n"
@@ -906,8 +910,15 @@
     "    executable = cp,\n"
     "    arguments = [args],\n"
     ")\n"
-    "```"
-  autofix: false
+    "```\n\n"
+    "Simple cases are fixed automatically: a `File.path` passed to `args.add()`,\n"
+    "`args.add_all()`, `args.add_joined()` or `executable`, optionally formatted\n"
+    "into a string literal or wrapped in a list comprehension, is replaced by the\n"
+    "`File` object. Variables assigned from `ctx.actions.declare_directory()` are\n"
+    "passed with `expand_directories = False`. Other directories have to be fixed\n"
+    "manually, since `args.add()` rejects them and `args.add_all()` expands them by\n"
+    "default."
+  autofix: true
 }
 
 warnings: {
diff --git a/warn/warn_bazel.go b/warn/warn_bazel.go
index c87ddbc..a082968 100644
--- a/warn/warn_bazel.go
+++ b/warn/warn_bazel.go
@@ -280,12 +280,84 @@
 	return findings
 }
 
-// pathStringAttrs lists attributes that return a path string which may contain a configuration
-// prefix (such as `bazel-out/k8-fastbuild/bin`) and is therefore not rewritten by path mapping:
-// `File.path`, `File.dirname`, `File.root.path`, `ctx.bin_dir.path` and `ctx.genfiles_dir.path`.
-var pathStringAttrs = map[string]bool{
-	"path":    true,
-	"dirname": true,
+// pathStringKind classifies expressions that return a path string which may contain a
+// configuration prefix (such as `bazel-out/k8-fastbuild/bin`) and is therefore not rewritten by
+// path mapping.
+type pathStringKind int
+
+const (
+	notPathString pathStringKind = iota
+	filePath                     // `file.path`
+	fileDirname                  // `file.dirname`
+	fileRootPath                 // `file.root.path`
+	ctxDirPath                   // `ctx.bin_dir.path` or `ctx.genfiles_dir.path`
+)
+
+// pathStringContext describes where a path string is used.
+type pathStringContext int
+
+const (
+	argsContext       pathStringContext = iota // `args.add()`, `args.add_all()` or `args.add_joined()`
+	executableContext                          // `executable` of `ctx.actions.run()`
+	argumentsContext                           // `arguments` of `ctx.actions.run()` or `ctx.actions.run_shell()`
+	commandContext                             // `command` of `ctx.actions.run_shell()`
+)
+
+// classifyPathString returns the kind of path string the expression evaluates to. The parent is
+// used to skip method calls such as `ctx.path(...)` or `paths.dirname(...)`.
+func classifyPathString(expr, parent build.Expr) pathStringKind {
+	dot, ok := expr.(*build.DotExpr)
+	if !ok {
+		return notPathString
+	}
+	if call, ok := parent.(*build.CallExpr); ok && call.X == expr {
+		return notPathString
+	}
+	switch dot.Name {
+	case "dirname":
+		return fileDirname
+	case "path":
+		if inner, ok := dot.X.(*build.DotExpr); ok {
+			switch inner.Name {
+			case "root":
+				return fileRootPath
+			case "bin_dir", "genfiles_dir":
+				return ctxDirPath
+			}
+		}
+		return filePath
+	}
+	return notPathString
+}
+
+// pathStringMessage builds a message for a path string of the given kind used in the given context.
+func pathStringMessage(dot *build.DotExpr, kind pathStringKind, context pathStringContext, method string) string {
+	expr := build.FormatString(dot)
+	receiver := build.FormatString(dot.X)
+	msg := fmt.Sprintf("%q is not rewritten by path mapping. ", expr)
+	switch kind {
+	case fileDirname:
+		return msg + fmt.Sprintf(`Add %q to "ctx.actions.args()" with a "map_each" callback that returns "file.dirname" instead.`, receiver)
+	case fileRootPath:
+		file := build.FormatString(dot.X.(*build.DotExpr).X)
+		return msg + fmt.Sprintf(`Add %q to "ctx.actions.args()" with a "map_each" callback that returns "file.root.path" instead.`, file)
+	case ctxDirPath:
+		return msg + `Compute it from an output "File" in a "map_each" callback via "file.root.path" instead.`
+	}
+	switch context {
+	case argsContext:
+		if strings.HasSuffix(method, ".add") {
+			return msg + fmt.Sprintf(`Pass %q to "%s()" instead, or use "%s_all([%s], expand_directories = False)" if it is a directory.`,
+				receiver, method, method, receiver)
+		}
+		return msg + fmt.Sprintf(`Pass %q to "%s()" instead (with "expand_directories = False" if it is a directory).`, receiver, method)
+	case executableContext:
+		return msg + fmt.Sprintf(`Pass %q as "executable" instead.`, receiver)
+	case argumentsContext:
+		return msg + fmt.Sprintf(`Add %q to "ctx.actions.args()" and pass the "Args" object in "arguments" instead.`, receiver)
+	default:
+		return msg + fmt.Sprintf(`Add %q to "ctx.actions.args()", pass the "Args" object in "arguments" and refer to it as "$1", "$2", ... in "command" instead.`, receiver)
+	}
 }
 
 // isCtxActionsArgs reports whether expr is a `ctx.actions.args()` object. Besides the detected
@@ -298,6 +370,279 @@
 	return ok && ident.Name == "args"
 }
 
+// collectDeclaredDirectories returns the names of all variables that are assigned the result of
+// `ctx.actions.declare_directory()` anywhere in the file.
+func collectDeclaredDirectories(f *build.File) map[string]bool {
+	directories := make(map[string]bool)
+	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
+		assign, ok := expr.(*build.AssignExpr)
+		if !ok {
+			return
+		}
+		ident, ok := assign.LHS.(*build.Ident)
+		if !ok {
+			return
+		}
+		call, ok := assign.RHS.(*build.CallExpr)
+		if !ok {
+			return
+		}
+		if dot, ok := call.X.(*build.DotExpr); ok && dot.Name == "declare_directory" {
+			directories[ident.Name] = true
+		}
+	})
+	return directories
+}
+
+// isValidArgsFormat reports whether str can be used as a `format` parameter of
+// `ctx.actions.args()` methods, i.e. it contains exactly one `%s` and no other `%`.
+func isValidArgsFormat(str string) bool {
+	return strings.Count(str, "%") == 1 && strings.Contains(str, "%s")
+}
+
+// splitFormattedPath checks whether expr formats a single `file.path` expression into a string
+// literal, e.g. `"-I%s" % file.path`, `"-I" + file.path`, `file.path + ".d"` or
+// `"-I{}".format(file.path)`. It returns the equivalent `format` string for `ctx.actions.args()`
+// and the formatted `file.path` expression.
+func splitFormattedPath(expr build.Expr) (string, *build.DotExpr, bool) {
+	switch node := expr.(type) {
+	case *build.ParenExpr:
+		return splitFormattedPath(node.X)
+	case *build.BinaryExpr:
+		switch node.Op {
+		case "%":
+			str, ok := node.X.(*build.StringExpr)
+			if !ok || !isValidArgsFormat(str.Value) {
+				return "", nil, false
+			}
+			dot, ok := node.Y.(*build.DotExpr)
+			if !ok || classifyPathString(dot, nil) != filePath {
+				return "", nil, false
+			}
+			return str.Value, dot, true
+		case "+":
+			var format strings.Builder
+			var dot *build.DotExpr
+			for _, operand := range flattenConcatenation(node) {
+				switch operand := operand.(type) {
+				case *build.StringExpr:
+					if strings.Contains(operand.Value, "%") {
+						return "", nil, false
+					}
+					format.WriteString(operand.Value)
+				case *build.DotExpr:
+					if dot != nil || classifyPathString(operand, nil) != filePath {
+						return "", nil, false
+					}
+					dot = operand
+					format.WriteString("%s")
+				default:
+					return "", nil, false
+				}
+			}
+			if dot == nil {
+				return "", nil, false
+			}
+			return format.String(), dot, true
+		}
+	case *build.CallExpr:
+		method, ok := node.X.(*build.DotExpr)
+		if !ok || method.Name != "format" || len(node.List) != 1 {
+			return "", nil, false
+		}
+		str, ok := method.X.(*build.StringExpr)
+		if !ok || strings.Count(str.Value, "{}") != 1 || strings.Count(str.Value, "{")+strings.Count(str.Value, "}") != 2 || strings.Contains(str.Value, "%") {
+			return "", nil, false
+		}
+		dot, ok := node.List[0].(*build.DotExpr)
+		if !ok || classifyPathString(dot, nil) != filePath {
+			return "", nil, false
+		}
+		return strings.Replace(str.Value, "{}", "%s", 1), dot, true
+	}
+	return "", nil, false
+}
+
+// flattenConcatenation returns the operands of a chain of `+` operations.
+func flattenConcatenation(expr build.Expr) []build.Expr {
+	if binary, ok := expr.(*build.BinaryExpr); ok && binary.Op == "+" {
+		return append(flattenConcatenation(binary.X), flattenConcatenation(binary.Y)...)
+	}
+	if paren, ok := expr.(*build.ParenExpr); ok {
+		return flattenConcatenation(paren.X)
+	}
+	return []build.Expr{expr}
+}
+
+// simpleComprehension checks whether expr is a list comprehension of the form `[body for var in seq]`
+// and returns the loop variable, the body and the sequence.
+func simpleComprehension(expr build.Expr) (*build.Ident, build.Expr, build.Expr, bool) {
+	comp, ok := expr.(*build.Comprehension)
+	if !ok || comp.Curly || len(comp.Clauses) != 1 {
+		return nil, nil, nil, false
+	}
+	forClause, ok := comp.Clauses[0].(*build.ForClause)
+	if !ok {
+		return nil, nil, nil, false
+	}
+	loopVar, ok := forClause.Vars.(*build.Ident)
+	if !ok {
+		return nil, nil, nil, false
+	}
+	return loopVar, comp.Body, forClause.X, true
+}
+
+// isPathOf reports whether expr is `<ident>.path` for the given identifier.
+func isPathOf(expr build.Expr, ident *build.Ident) (*build.DotExpr, bool) {
+	dot, ok := expr.(*build.DotExpr)
+	if !ok || classifyPathString(dot, nil) != filePath {
+		return nil, false
+	}
+	x, ok := dot.X.(*build.Ident)
+	return dot, ok && x.Name == ident.Name
+}
+
+// pathMappingFixer collects fixes for a single `args.add()`, `args.add_all()` or `args.add_joined()`
+// call. A fix either replaces a single node (e.g. `file.path` with `file`) or rewrites the whole
+// call (e.g. to add a `format` parameter).
+type pathMappingFixer struct {
+	call        *build.CallExpr
+	callPtr     *build.Expr
+	method      string
+	directories map[string]bool
+	newList     []build.Expr                         // modified copy of call.List
+	newMethod   string                               // method name of the rewritten call, if it changes
+	rewriteCall bool                                 // the whole call needs to be replaced
+	fixed       map[*build.DotExpr]LinterReplacement // node-level fixes
+	broken      bool                                 // a safe fix is not possible
+}
+
+func newPathMappingFixer(callPtr *build.Expr, call *build.CallExpr, method string, directories map[string]bool) *pathMappingFixer {
+	return &pathMappingFixer{
+		call:        call,
+		callPtr:     callPtr,
+		method:      method,
+		directories: directories,
+		newList:     append([]build.Expr{}, call.List...),
+		newMethod:   method,
+		fixed:       make(map[*build.DotExpr]LinterReplacement),
+	}
+}
+
+func (fx *pathMappingFixer) hasKeyword(name string) bool {
+	_, _, param := getParam(fx.call.List, name)
+	return param != nil
+}
+
+func (fx *pathMappingFixer) isDirectory(expr build.Expr) bool {
+	ident, ok := expr.(*build.Ident)
+	return ok && fx.directories[ident.Name]
+}
+
+func (fx *pathMappingFixer) addKeyword(name string, value build.Expr) {
+	fx.newList = append(fx.newList, makeKeyword(value, name))
+	fx.rewriteCall = true
+}
+
+// fixPositional tries to fix the i-th positional argument of the call.
+func (fx *pathMappingFixer) fixPositional(i int) {
+	arg := fx.call.List[i]
+	switch fx.method {
+	case "add":
+		// `args.add(file.path)`, `args.add("--flag", file.path)`, `args.add("--flag=%s" % file.path)`
+		if dot, ok := arg.(*build.DotExpr); ok && classifyPathString(dot, nil) == filePath {
+			fx.newList[i] = dot.X
+			fx.fixed[dot] = LinterReplacement{&fx.call.List[i], dot.X}
+			if fx.isDirectory(dot.X) {
+				// `args.add()` doesn't accept directories, use `args.add_all([...], expand_directories = False)`
+				fx.newMethod = "add_all"
+				fx.rewriteCall = true
+			}
+			return
+		}
+		if format, dot, ok := splitFormattedPath(arg); ok && !fx.hasKeyword("format") && !fx.isDirectory(dot.X) {
+			fx.newList[i] = dot.X
+			fx.fixed[dot] = LinterReplacement{}
+			fx.addKeyword("format", &build.StringExpr{Value: format})
+		}
+	case "add_all", "add_joined":
+		if i != 0 || fx.hasKeyword("map_each") {
+			return
+		}
+		switch values := arg.(type) {
+		case *build.ListExpr:
+			// `args.add_all([a.path, b.path])`
+			newValues := *values
+			newValues.List = append([]build.Expr{}, values.List...)
+			for j, value := range values.List {
+				dot, ok := value.(*build.DotExpr)
+				if !ok || classifyPathString(dot, nil) != filePath {
+					continue
+				}
+				newValues.List[j] = dot.X
+				fx.fixed[dot] = LinterReplacement{&values.List[j], dot.X}
+				if fx.isDirectory(dot.X) && !fx.hasKeyword("expand_directories") {
+					fx.rewriteCall = true
+				}
+			}
+			if fx.rewriteCall {
+				fx.newList[i] = &newValues
+				if !fx.hasKeyword("expand_directories") {
+					fx.addKeyword("expand_directories", &build.Ident{Name: "False"})
+				}
+			}
+		case *build.Comprehension:
+			// `args.add_all([f.path for f in files])`, `args.add_all(["-I" + f.path for f in files])`
+			loopVar, body, seq, ok := simpleComprehension(values)
+			if !ok {
+				return
+			}
+			if dot, ok := isPathOf(body, loopVar); ok {
+				fx.newList[i] = seq
+				fx.fixed[dot] = LinterReplacement{&fx.call.List[i], seq}
+				return
+			}
+			format, dot, ok := splitFormattedPath(body)
+			if !ok || fx.hasKeyword("format_each") {
+				return
+			}
+			if _, ok := isPathOf(dot, loopVar); !ok {
+				return
+			}
+			fx.newList[i] = seq
+			fx.fixed[dot] = LinterReplacement{}
+			fx.addKeyword("format_each", &build.StringExpr{Value: format})
+		}
+	}
+}
+
+// replacement returns the fix for the given node, if any.
+func (fx *pathMappingFixer) replacement(dot *build.DotExpr) []LinterReplacement {
+	fix, ok := fx.fixed[dot]
+	if !ok {
+		return nil
+	}
+	if !fx.rewriteCall {
+		return []LinterReplacement{fix}
+	}
+	if fx.newMethod != fx.method && fx.hasKeyword("format") {
+		// `args.add(dir.path, format = ...)` can't be converted to `args.add_all()`
+		return nil
+	}
+	newCall := *fx.call
+	newCall.List = fx.newList
+	if fx.newMethod != fx.method {
+		newDot := *fx.call.X.(*build.DotExpr)
+		newDot.Name = fx.newMethod
+		newCall.X = &newDot
+		newCall.List = []build.Expr{
+			&build.ListExpr{List: fx.newList},
+			makeKeyword(&build.Ident{Name: "False"}, "expand_directories"),
+		}
+	}
+	return []LinterReplacement{{fx.callPtr, &newCall}}
+}
+
 // pathMappingWarning reports path strings used in action command lines. Path mapping
 // (https://github.com/bazelbuild/bazel/discussions/22658) can only rewrite `File` objects passed to
 // `ctx.actions.args()`, so strings obtained from `File.path`, `File.dirname` etc. break actions
@@ -308,29 +653,27 @@
 	}
 
 	types := DetectTypes(f)
+	directories := collectDeclaredDirectories(f)
 	findings := []*LinterFinding{}
 
-	// checkArgument reports every path string expression inside a command line argument.
-	checkArgument := func(arg build.Expr) {
+	// report reports every path string expression inside a command line argument.
+	report := func(arg build.Expr, context pathStringContext, method string, replacement func(dot *build.DotExpr) []LinterReplacement) {
 		build.Walk(arg, func(expr build.Expr, stack []build.Expr) {
-			dot, ok := expr.(*build.DotExpr)
-			if !ok || !pathStringAttrs[dot.Name] {
+			var parent build.Expr
+			if len(stack) > 0 {
+				parent = stack[len(stack)-1]
+			}
+			kind := classifyPathString(expr, parent)
+			if kind == notPathString {
 				return
 			}
-			if len(stack) > 0 {
-				// Skip method calls such as `ctx.path(...)` or `paths.dirname(...)`
-				if call, ok := stack[len(stack)-1].(*build.CallExpr); ok && call.X == expr {
-					return
-				}
-			}
-			findings = append(findings, makeLinterFinding(dot, fmt.Sprintf(
-				`%q is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-				build.FormatString(dot))))
+			dot := expr.(*build.DotExpr)
+			findings = append(findings, makeLinterFinding(dot, pathStringMessage(dot, kind, context, method), replacement(dot)...))
 		})
 	}
 
-	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
-		call, ok := expr.(*build.CallExpr)
+	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
+		call, ok := (*expr).(*build.CallExpr)
 		if !ok {
 			return
 		}
@@ -347,26 +690,37 @@
 			default:
 				return
 			}
-			for _, arg := range call.List {
-				if assign, ok := arg.(*build.AssignExpr); ok {
-					arg = assign.RHS
+			fixer := newPathMappingFixer(expr, call, dot.Name, directories)
+			for i, arg := range call.List {
+				if _, ok := arg.(*build.AssignExpr); !ok {
+					fixer.fixPositional(i)
 				}
-				checkArgument(arg)
+			}
+			for _, arg := range call.List {
+				report(makePositional(arg), argsContext, build.FormatString(dot), fixer.replacement)
 			}
 		case types[dot.X] == CtxActions:
 			// `ctx.actions.run(...)` and `ctx.actions.run_shell(...)`
-			var params []string
+			noFix := func(*build.DotExpr) []LinterReplacement { return nil }
 			switch dot.Name {
 			case "run":
-				params = []string{"executable", "arguments"}
+				if _, _, param := getParam(call.List, "executable"); param != nil {
+					report(param.RHS, executableContext, dot.Name, func(dot *build.DotExpr) []LinterReplacement {
+						if dot == param.RHS && classifyPathString(dot, nil) == filePath {
+							return []LinterReplacement{{&param.RHS, dot.X}}
+						}
+						return nil
+					})
+				}
+				if _, _, param := getParam(call.List, "arguments"); param != nil {
+					report(param.RHS, argumentsContext, dot.Name, noFix)
+				}
 			case "run_shell":
-				params = []string{"command", "arguments"}
-			default:
-				return
-			}
-			for _, name := range params {
-				if _, _, param := getParam(call.List, name); param != nil {
-					checkArgument(param.RHS)
+				if _, _, param := getParam(call.List, "command"); param != nil {
+					report(param.RHS, commandContext, dot.Name, noFix)
+				}
+				if _, _, param := getParam(call.List, "arguments"); param != nil {
+					report(param.RHS, argumentsContext, dot.Name, noFix)
 				}
 			}
 		}
diff --git a/warn/warn_bazel_test.go b/warn/warn_bazel_test.go
index 85a78be..dc3a4d1 100644
--- a/warn/warn_bazel_test.go
+++ b/warn/warn_bazel_test.go
@@ -247,7 +247,7 @@
 }
 
 func TestPathMapping(t *testing.T) {
-	checkFindings(t, "path-mapping", `
+	checkFindingsAndFix(t, "path-mapping", `
 load("@bazel_skylib//lib:paths.bzl", "paths")
 
 def _dirname(file):
@@ -259,6 +259,7 @@
 def _impl(ctx):
     args = ctx.actions.args()
     actions = ctx.actions
+    out_dir = ctx.actions.declare_directory("out")
 
     # Compatible with path mapping
     args.add(src)
@@ -280,18 +281,42 @@
         arguments = [args],
     )
 
-    # Incompatible with path mapping
+    # Incompatible with path mapping, fixed automatically
     args.add(src.path)
     args.add("--out", out.path)
-    args.add(src.dirname)
-    args.add(dir.path)
+    args.add(src.path, format = "--src=%s")
+    args.add("--src=%s" % src.path)
+    args.add("--src=" + src.path)
+    args.add(src.path + ".d")
+    args.add("-I" + src.path + "/include")
+    args.add("--src={}".format(src.path))
+    args.add(out_dir.path)
+    args.add("--out", out_dir.path)
+    args.add_all([src.path, out.path])
     args.add_all([src.path for src in srcs])
+    args.add_all([src.path for src in srcs], format_each = "-I%s")
+    args.add_all(["-I" + inc.path for inc in incs])
     args.add_joined([src.path for src in srcs], join_with = ",")
+    args.add_all([src.path, out_dir.path])
+    ctx.actions.run(
+        outputs = [out],
+        executable = tool.path,
+        arguments = [args],
+    )
+
+    # Incompatible with path mapping, not fixed automatically
+    args.add(src.dirname)
     args.add("--bin=%s" % ctx.bin_dir.path)
     args.add("--gen={}".format(ctx.genfiles_dir.path))
     args.add("--root=" + src.root.path)
     args.add(paths.join(ctx.bin_dir.path, "include"))
     args.add(src.path if src else out.path)
+    args.add("--src=%s" % src.path, format = "%s")
+    args.add("%s=%s" % (name, src.path))
+    args.add(out_dir.path, format = "--out=%s")
+    args.add_all([src.path for src in srcs if src])
+    args.add_all([src.path for src in srcs], map_each = _dirname)
+    args.add_all(["-I" + inc.path for inc in incs], format_each = "%s")
     ctx.actions.run(
         outputs = [out],
         executable = tool.path,
@@ -316,28 +341,142 @@
         arguments = [args],
         progress_message = "Building %s" % out.path,
     )
+`, `
+load("@bazel_skylib//lib:paths.bzl", "paths")
+
+def _dirname(file):
+    return file.dirname
+
+def _add_srcs(args, srcs):
+    args.add_all(srcs)
+
+def _impl(ctx):
+    args = ctx.actions.args()
+    actions = ctx.actions
+    out_dir = ctx.actions.declare_directory("out")
+
+    # Compatible with path mapping
+    args.add(src)
+    args.add("--out", out)
+    args.add_all(dirs, expand_directories = False)
+    args.add_all([src], map_each = _dirname)
+    args.add_all(srcs, format_each = "--src=%s")
+    args.add(src.short_path)
+    args.add(src.basename)
+    args.add(paths.dirname(src.short_path))
+    ctx.actions.run(
+        outputs = [out],
+        executable = ctx.executable._tool,
+        arguments = [args],
+    )
+    ctx.actions.run_shell(
+        outputs = [out],
+        command = "cp $1 $2",
+        arguments = [args],
+    )
+
+    # Incompatible with path mapping, fixed automatically
+    args.add(src)
+    args.add("--out", out)
+    args.add(src, format = "--src=%s")
+    args.add(src, format = "--src=%s")
+    args.add(src, format = "--src=%s")
+    args.add(src, format = "%s.d")
+    args.add(src, format = "-I%s/include")
+    args.add(src, format = "--src=%s")
+    args.add_all([out_dir], expand_directories = False)
+    args.add_all(["--out", out_dir], expand_directories = False)
+    args.add_all([src, out])
+    args.add_all(srcs)
+    args.add_all(srcs, format_each = "-I%s")
+    args.add_all(incs, format_each = "-I%s")
+    args.add_joined(srcs, join_with = ",")
+    args.add_all([src, out_dir], expand_directories = False)
+    ctx.actions.run(
+        outputs = [out],
+        executable = tool,
+        arguments = [args],
+    )
+
+    # Incompatible with path mapping, not fixed automatically
+    args.add(src.dirname)
+    args.add("--bin=%s" % ctx.bin_dir.path)
+    args.add("--gen={}".format(ctx.genfiles_dir.path))
+    args.add("--root=" + src.root.path)
+    args.add(paths.join(ctx.bin_dir.path, "include"))
+    args.add(src.path if src else out.path)
+    args.add("--src=%s" % src.path, format = "%s")
+    args.add("%s=%s" % (name, src.path))
+    args.add(out_dir.path, format = "--out=%s")
+    args.add_all([src.path for src in srcs if src])
+    args.add_all([src.path for src in srcs], map_each = _dirname)
+    args.add_all(["-I" + inc.path for inc in incs], format_each = "%s")
+    ctx.actions.run(
+        outputs = [out],
+        executable = tool,
+        arguments = [src.path, "--out", out.path],
+    )
+    ctx.actions.run_shell(
+        outputs = [out],
+        command = "cp %s %s" % (src.path, out.path),
+    )
+    actions.run_shell(
+        outputs = [out],
+        command = "cp $1 " + out.dirname,
+        arguments = [src.path],
+    )
+
+    # Unrelated
+    other.add(src.path)
+    ctx.actions.other(arguments = [src.path])
+    ctx.actions.run(
+        outputs = [out],
+        executable = ctx.executable._tool,
+        arguments = [args],
+        progress_message = "Building %s" % out.path,
+    )
 `,
 		[]string{
-			`:7: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:34: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:35: "out.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:36: "src.dirname" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:37: "dir.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:38: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:39: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:40: "ctx.bin_dir.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:41: "ctx.genfiles_dir.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:42: "src.root.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:43: "ctx.bin_dir.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:44: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:44: "out.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:47: "tool.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:48: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:48: "out.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:52: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:52: "out.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:56: "out.dirname" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
-			`:57: "src.path" is a path string that is not rewritten by path mapping. Pass the "File" object to "ctx.actions.args()" or compute the path in a "map_each" callback instead.`,
+			`:7: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:35: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:36: "out.path" is not rewritten by path mapping. Pass "out" to "args.add()" instead, or use "args.add_all([out], expand_directories = False)" if it is a directory.`,
+			`:37: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:38: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:39: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:40: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:41: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:42: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:43: "out_dir.path" is not rewritten by path mapping. Pass "out_dir" to "args.add()" instead, or use "args.add_all([out_dir], expand_directories = False)" if it is a directory.`,
+			`:44: "out_dir.path" is not rewritten by path mapping. Pass "out_dir" to "args.add()" instead, or use "args.add_all([out_dir], expand_directories = False)" if it is a directory.`,
+			`:45: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:45: "out.path" is not rewritten by path mapping. Pass "out" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:46: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:47: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:48: "inc.path" is not rewritten by path mapping. Pass "inc" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:49: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_joined()" instead (with "expand_directories = False" if it is a directory).`,
+			`:50: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:50: "out_dir.path" is not rewritten by path mapping. Pass "out_dir" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:53: "tool.path" is not rewritten by path mapping. Pass "tool" as "executable" instead.`,
+			`:58: "src.dirname" is not rewritten by path mapping. Add "src" to "ctx.actions.args()" with a "map_each" callback that returns "file.dirname" instead.`,
+			`:59: "ctx.bin_dir.path" is not rewritten by path mapping. Compute it from an output "File" in a "map_each" callback via "file.root.path" instead.`,
+			`:60: "ctx.genfiles_dir.path" is not rewritten by path mapping. Compute it from an output "File" in a "map_each" callback via "file.root.path" instead.`,
+			`:61: "src.root.path" is not rewritten by path mapping. Add "src" to "ctx.actions.args()" with a "map_each" callback that returns "file.root.path" instead.`,
+			`:62: "ctx.bin_dir.path" is not rewritten by path mapping. Compute it from an output "File" in a "map_each" callback via "file.root.path" instead.`,
+			`:63: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:63: "out.path" is not rewritten by path mapping. Pass "out" to "args.add()" instead, or use "args.add_all([out], expand_directories = False)" if it is a directory.`,
+			`:64: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:65: "src.path" is not rewritten by path mapping. Pass "src" to "args.add()" instead, or use "args.add_all([src], expand_directories = False)" if it is a directory.`,
+			`:66: "out_dir.path" is not rewritten by path mapping. Pass "out_dir" to "args.add()" instead, or use "args.add_all([out_dir], expand_directories = False)" if it is a directory.`,
+			`:67: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:68: "src.path" is not rewritten by path mapping. Pass "src" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:69: "inc.path" is not rewritten by path mapping. Pass "inc" to "args.add_all()" instead (with "expand_directories = False" if it is a directory).`,
+			`:72: "tool.path" is not rewritten by path mapping. Pass "tool" as "executable" instead.`,
+			`:73: "src.path" is not rewritten by path mapping. Add "src" to "ctx.actions.args()" and pass the "Args" object in "arguments" instead.`,
+			`:73: "out.path" is not rewritten by path mapping. Add "out" to "ctx.actions.args()" and pass the "Args" object in "arguments" instead.`,
+			`:77: "src.path" is not rewritten by path mapping. Add "src" to "ctx.actions.args()", pass the "Args" object in "arguments" and refer to it as "$1", "$2", ... in "command" instead.`,
+			`:77: "out.path" is not rewritten by path mapping. Add "out" to "ctx.actions.args()", pass the "Args" object in "arguments" and refer to it as "$1", "$2", ... in "command" instead.`,
+			`:81: "out.dirname" is not rewritten by path mapping. Add "out" to "ctx.actions.args()" with a "map_each" callback that returns "file.dirname" instead.`,
+			`:82: "src.path" is not rewritten by path mapping. Add "src" to "ctx.actions.args()" and pass the "Args" object in "arguments" instead.`,
 		},
 		scopeBzl)
 }