internal/refactor/inline: ignore line directives

Line directives should be ignored (at both caller
and callee) as they would otherwise cause the
inliner to make a mess of the caller file.
This change ignores them consistently in the tests,
and adds a heuristic assertion to reject inputs
that seem to be affected by line directives.

Plus: a test.

Change-Id: I1a9bffd935fd7b288c47647304a2a6529779434b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/528298
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go
index 3e9f397..9fff849 100644
--- a/internal/refactor/inline/callee.go
+++ b/internal/refactor/inline/callee.go
@@ -70,6 +70,10 @@
 // This design allows separate analysis of callers and callees in the
 // golang.org/x/tools/go/analysis framework: the inlining information
 // about a callee can be recorded as a "fact".
+//
+// The content should be the actual input to the compiler, not the
+// apparent source file according to any //line directives that
+// may be present within it.
 func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error) {
 	checkInfoFields(info)
 
@@ -322,11 +326,9 @@
 	// callee file content; all we need is "package _; func f() { ... }".
 	// This reduces the size of analysis facts.
 	//
-	// (For ease of debugging we could insert a //line directive after
-	// the package decl but it seems more trouble than it's worth.)
-	//
 	// Offsets in the callee information are "relocatable"
 	// since they are all relative to the FuncDecl.
+
 	content = append([]byte("package _\n"),
 		content[offsetOf(fset, decl.Pos()):offsetOf(fset, decl.End())]...)
 	// Sanity check: re-parse the compacted content.
@@ -386,7 +388,7 @@
 	fnobj, ok := info.Defs[decl.Name]
 	if !ok {
 		panic(fmt.Sprintf("%s: no func object for %q",
-			fset.Position(decl.Name.Pos()), decl.Name)) // ill-typed?
+			fset.PositionFor(decl.Name.Pos(), false), decl.Name)) // ill-typed?
 	}
 
 	paramInfos := make(map[*types.Var]*paramInfo)
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 02ab43b..e2f1a2a 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -305,7 +305,7 @@
 	Info    *types.Info
 	File    *ast.File
 	Call    *ast.CallExpr
-	Content []byte
+	Content []byte // source of file containing
 }
 
 // Inline inlines the called function (callee) into the function call (caller)
@@ -325,7 +325,11 @@
 	}
 	logf("inline %s @ %v",
 		debugFormatNode(caller.Fset, caller.Call),
-		caller.Fset.Position(caller.Call.Lparen))
+		caller.Fset.PositionFor(caller.Call.Lparen, false))
+
+	if !consistentOffsets(caller) {
+		return nil, fmt.Errorf("internal error: caller syntax positions are inconsistent with file content (did you forget to use FileSet.PositionFor when computing the file name?)")
+	}
 
 	res, err := inline(logf, caller, &callee.impl)
 	if err != nil {
@@ -611,7 +615,7 @@
 			if found.Pos().IsValid() {
 				return nil, fmt.Errorf("cannot inline because built-in %q is shadowed in caller by a %s (line %d)",
 					obj.Name, objectKind(found),
-					caller.Fset.Position(found.Pos()).Line)
+					caller.Fset.PositionFor(found.Pos(), false).Line)
 			}
 
 		} else {
@@ -627,7 +631,7 @@
 					if !isPkgLevel(found) {
 						return nil, fmt.Errorf("cannot inline because %q is shadowed in caller by a %s (line %d)",
 							obj.Name, objectKind(found),
-							caller.Fset.Position(found.Pos()).Line)
+							caller.Fset.PositionFor(found.Pos(), false).Line)
 					}
 				} else {
 					// Cross-package reference.
@@ -957,7 +961,7 @@
 						// function (if any) and is indeed referenced
 						// only by the call.
 						logf("keeping param %q: arg contains perhaps the last reference to possible caller local %v @ %v",
-							param.info.Name, v, caller.Fset.Position(v.Pos()))
+							param.info.Name, v, caller.Fset.PositionFor(v.Pos(), false))
 						continue next
 					}
 				}
@@ -2055,3 +2059,22 @@
 	}
 	return true
 }
+
+// consistentOffsets reports whether the portion of caller.Content
+// that corresponds to caller.Call can be parsed as a call expression.
+// If not, the client has provided inconsistent information, possibly
+// because they forgot to ignore line directives when computing the
+// filename enclosing the call.
+// This is just a heuristic.
+func consistentOffsets(caller *Caller) bool {
+	start := offsetOf(caller.Fset, caller.Call.Pos())
+	end := offsetOf(caller.Fset, caller.Call.End())
+	if !(0 < start && start < end && end <= len(caller.Content)) {
+		return false
+	}
+	expr, err := parser.ParseExpr(string(caller.Content[start:end]))
+	if err != nil {
+		return false
+	}
+	return is[*ast.CallExpr](expr)
+}
diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go
index 430ee49..cb52f31 100644
--- a/internal/refactor/inline/inline_test.go
+++ b/internal/refactor/inline/inline_test.go
@@ -93,7 +93,7 @@
 						continue
 					}
 					for _, note := range notes {
-						posn := pkg.Fset.Position(note.Pos)
+						posn := pkg.Fset.PositionFor(note.Pos, false)
 						if note.Name != "inline" {
 							t.Errorf("%s: invalid marker @%s", posn, note.Name)
 							continue
diff --git a/internal/refactor/inline/testdata/line-directives.txtar b/internal/refactor/inline/testdata/line-directives.txtar
new file mode 100644
index 0000000..66ae9ed
--- /dev/null
+++ b/internal/refactor/inline/testdata/line-directives.txtar
@@ -0,0 +1,35 @@
+Test of line directives in caller and caller.
+Neither should have any effect on inlining.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/b"
+
+func A() {
+//line b2.go:3:3
+	b.F() //@ inline(re"F", result)
+}
+
+-- b/b.go --
+package b
+
+//line b2.go:1:1
+func F() { println("hi") }
+
+-- b/b2.go --
+package b
+
+func NotWhatYouWereLookingFor() {}
+
+-- result --
+package a
+
+func A() {
+//line b2.go:3:3
+	println("hi") //@ inline(re"F", result)
+}