--- /dev/null
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package modfile implements a parser and formatter for go.mod files.
+//
+// The go.mod syntax is described in
+// https://golang.org/cmd/go/#hdr-The_go_mod_file.
+//
+// The Parse and ParseLax functions both parse a go.mod file and return an
+// abstract syntax tree. ParseLax ignores unknown statements and may be used to
+// parse go.mod files that may have been developed with newer versions of Go.
+//
+// The File struct returned by Parse and ParseLax represent an abstract
+// go.mod file. File has several methods like AddNewRequire and DropReplace
+// that can be used to programmatically edit a file.
+//
+// The Format function formats a File back to a byte slice which can be
+// written to a file.
+package modfile
+
+import (
+ "errors"
+ "fmt"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "golang.org/x/mod/internal/lazyregexp"
+ "golang.org/x/mod/module"
+)
+
+// A File is the parsed, interpreted form of a go.mod file.
+type File struct {
+ Module *Module
+ Go *Go
+ Require []*Require
+ Exclude []*Exclude
+ Replace []*Replace
+
+ Syntax *FileSyntax
+}
+
+// A Module is the module statement.
+type Module struct {
+ Mod module.Version
+ Syntax *Line
+}
+
+// A Go is the go statement.
+type Go struct {
+ Version string // "1.23"
+ Syntax *Line
+}
+
+// A Require is a single require statement.
+type Require struct {
+ Mod module.Version
+ Indirect bool // has "// indirect" comment
+ Syntax *Line
+}
+
+// An Exclude is a single exclude statement.
+type Exclude struct {
+ Mod module.Version
+ Syntax *Line
+}
+
+// A Replace is a single replace statement.
+type Replace struct {
+ Old module.Version
+ New module.Version
+ Syntax *Line
+}
+
+func (f *File) AddModuleStmt(path string) error {
+ if f.Syntax == nil {
+ f.Syntax = new(FileSyntax)
+ }
+ if f.Module == nil {
+ f.Module = &Module{
+ Mod: module.Version{Path: path},
+ Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
+ }
+ } else {
+ f.Module.Mod.Path = path
+ f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
+ }
+ return nil
+}
+
+func (f *File) AddComment(text string) {
+ if f.Syntax == nil {
+ f.Syntax = new(FileSyntax)
+ }
+ f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
+ Comments: Comments{
+ Before: []Comment{
+ {
+ Token: text,
+ },
+ },
+ },
+ })
+}
+
+type VersionFixer func(path, version string) (string, error)
+
+// Parse parses the data, reported in errors as being from file,
+// into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
+func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
+ return parseToFile(file, data, fix, true)
+}
+
+// ParseLax is like Parse but ignores unknown statements.
+// It is used when parsing go.mod files other than the main module,
+// under the theory that most statement types we add in the future will
+// only apply in the main module, like exclude and replace,
+// and so we get better gradual deployments if old go commands
+// simply ignore those statements when found in go.mod files
+// in dependencies.
+func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
+ return parseToFile(file, data, fix, false)
+}
+
+func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) {
+ fs, err := parse(file, data)
+ if err != nil {
+ return nil, err
+ }
+ f := &File{
+ Syntax: fs,
+ }
+
+ var errs ErrorList
+ for _, x := range fs.Stmt {
+ switch x := x.(type) {
+ case *Line:
+ f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict)
+
+ case *LineBlock:
+ if len(x.Token) > 1 {
+ if strict {
+ errs = append(errs, Error{
+ Filename: file,
+ Pos: x.Start,
+ Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
+ })
+ }
+ continue
+ }
+ switch x.Token[0] {
+ default:
+ if strict {
+ errs = append(errs, Error{
+ Filename: file,
+ Pos: x.Start,
+ Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
+ })
+ }
+ continue
+ case "module", "require", "exclude", "replace":
+ for _, l := range x.Line {
+ f.add(&errs, l, x.Token[0], l.Token, fix, strict)
+ }
+ }
+ }
+ }
+
+ if len(errs) > 0 {
+ return nil, errs
+ }
+ return f, nil
+}
+
+var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+
+func (f *File) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
+ // If strict is false, this module is a dependency.
+ // We ignore all unknown directives as well as main-module-only
+ // directives like replace and exclude. It will work better for
+ // forward compatibility if we can depend on modules that have unknown
+ // statements (presumed relevant only when acting as the main module)
+ // and simply ignore those statements.
+ if !strict {
+ switch verb {
+ case "module", "require", "go":
+ // want these even for dependency go.mods
+ default:
+ return
+ }
+ }
+
+ wrapModPathError := func(modPath string, err error) {
+ *errs = append(*errs, Error{
+ Filename: f.Syntax.Name,
+ Pos: line.Start,
+ ModPath: modPath,
+ Verb: verb,
+ Err: err,
+ })
+ }
+ wrapError := func(err error) {
+ *errs = append(*errs, Error{
+ Filename: f.Syntax.Name,
+ Pos: line.Start,
+ Err: err,
+ })
+ }
+ errorf := func(format string, args ...interface{}) {
+ wrapError(fmt.Errorf(format, args...))
+ }
+
+ switch verb {
+ default:
+ errorf("unknown directive: %s", verb)
+
+ case "go":
+ if f.Go != nil {
+ errorf("repeated go statement")
+ return
+ }
+ if len(args) != 1 {
+ errorf("go directive expects exactly one argument")
+ return
+ } else if !GoVersionRE.MatchString(args[0]) {
+ errorf("invalid go version '%s': must match format 1.23", args[0])
+ return
+ }
+
+ f.Go = &Go{Syntax: line}
+ f.Go.Version = args[0]
+ case "module":
+ if f.Module != nil {
+ errorf("repeated module statement")
+ return
+ }
+ f.Module = &Module{Syntax: line}
+ if len(args) != 1 {
+ errorf("usage: module module/path")
+ return
+ }
+ s, err := parseString(&args[0])
+ if err != nil {
+ errorf("invalid quoted string: %v", err)
+ return
+ }
+ f.Module.Mod = module.Version{Path: s}
+ case "require", "exclude":
+ if len(args) != 2 {
+ errorf("usage: %s module/path v1.2.3", verb)
+ return
+ }
+ s, err := parseString(&args[0])
+ if err != nil {
+ errorf("invalid quoted string: %v", err)
+ return
+ }
+ v, err := parseVersion(verb, s, &args[1], fix)
+ if err != nil {
+ wrapError(err)
+ return
+ }
+ pathMajor, err := modulePathMajor(s)
+ if err != nil {
+ wrapError(err)
+ return
+ }
+ if err := module.CheckPathMajor(v, pathMajor); err != nil {
+ wrapModPathError(s, err)
+ return
+ }
+ if verb == "require" {
+ f.Require = append(f.Require, &Require{
+ Mod: module.Version{Path: s, Version: v},
+ Syntax: line,
+ Indirect: isIndirect(line),
+ })
+ } else {
+ f.Exclude = append(f.Exclude, &Exclude{
+ Mod: module.Version{Path: s, Version: v},
+ Syntax: line,
+ })
+ }
+ case "replace":
+ arrow := 2
+ if len(args) >= 2 && args[1] == "=>" {
+ arrow = 1
+ }
+ if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
+ errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
+ return
+ }
+ s, err := parseString(&args[0])
+ if err != nil {
+ errorf("invalid quoted string: %v", err)
+ return
+ }
+ pathMajor, err := modulePathMajor(s)
+ if err != nil {
+ wrapModPathError(s, err)
+ return
+ }
+ var v string
+ if arrow == 2 {
+ v, err = parseVersion(verb, s, &args[1], fix)
+ if err != nil {
+ wrapError(err)
+ return
+ }
+ if err := module.CheckPathMajor(v, pathMajor); err != nil {
+ wrapModPathError(s, err)
+ return
+ }
+ }
+ ns, err := parseString(&args[arrow+1])
+ if err != nil {
+ errorf("invalid quoted string: %v", err)
+ return
+ }
+ nv := ""
+ if len(args) == arrow+2 {
+ if !IsDirectoryPath(ns) {
+ errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
+ return
+ }
+ if filepath.Separator == '/' && strings.Contains(ns, `\`) {
+ errorf("replacement directory appears to be Windows path (on a non-windows system)")
+ return
+ }
+ }
+ if len(args) == arrow+3 {
+ nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
+ if err != nil {
+ wrapError(err)
+ return
+ }
+ if IsDirectoryPath(ns) {
+ errorf("replacement module directory path %q cannot have version", ns)
+ return
+ }
+ }
+ f.Replace = append(f.Replace, &Replace{
+ Old: module.Version{Path: s, Version: v},
+ New: module.Version{Path: ns, Version: nv},
+ Syntax: line,
+ })
+ }
+}
+
+// isIndirect reports whether line has a "// indirect" comment,
+// meaning it is in go.mod only for its effect on indirect dependencies,
+// so that it can be dropped entirely once the effective version of the
+// indirect dependency reaches the given minimum version.
+func isIndirect(line *Line) bool {
+ if len(line.Suffix) == 0 {
+ return false
+ }
+ f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
+ return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
+}
+
+// setIndirect sets line to have (or not have) a "// indirect" comment.
+func setIndirect(line *Line, indirect bool) {
+ if isIndirect(line) == indirect {
+ return
+ }
+ if indirect {
+ // Adding comment.
+ if len(line.Suffix) == 0 {
+ // New comment.
+ line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
+ return
+ }
+
+ com := &line.Suffix[0]
+ text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
+ if text == "" {
+ // Empty comment.
+ com.Token = "// indirect"
+ return
+ }
+
+ // Insert at beginning of existing comment.
+ com.Token = "// indirect; " + text
+ return
+ }
+
+ // Removing comment.
+ f := strings.Fields(line.Suffix[0].Token)
+ if len(f) == 2 {
+ // Remove whole comment.
+ line.Suffix = nil
+ return
+ }
+
+ // Remove comment prefix.
+ com := &line.Suffix[0]
+ i := strings.Index(com.Token, "indirect;")
+ com.Token = "//" + com.Token[i+len("indirect;"):]
+}
+
+// IsDirectoryPath reports whether the given path should be interpreted
+// as a directory path. Just like on the go command line, relative paths
+// and rooted paths are directory paths; the rest are module paths.
+func IsDirectoryPath(ns string) bool {
+ // Because go.mod files can move from one system to another,
+ // we check all known path syntaxes, both Unix and Windows.
+ return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
+ strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
+ len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
+}
+
+// MustQuote reports whether s must be quoted in order to appear as
+// a single token in a go.mod line.
+func MustQuote(s string) bool {
+ for _, r := range s {
+ switch r {
+ case ' ', '"', '\'', '`':
+ return true
+
+ case '(', ')', '[', ']', '{', '}', ',':
+ if len(s) > 1 {
+ return true
+ }
+
+ default:
+ if !unicode.IsPrint(r) {
+ return true
+ }
+ }
+ }
+ return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
+}
+
+// AutoQuote returns s or, if quoting is required for s to appear in a go.mod,
+// the quotation of s.
+func AutoQuote(s string) string {
+ if MustQuote(s) {
+ return strconv.Quote(s)
+ }
+ return s
+}
+
+func parseString(s *string) (string, error) {
+ t := *s
+ if strings.HasPrefix(t, `"`) {
+ var err error
+ if t, err = strconv.Unquote(t); err != nil {
+ return "", err
+ }
+ } else if strings.ContainsAny(t, "\"'`") {
+ // Other quotes are reserved both for possible future expansion
+ // and to avoid confusion. For example if someone types 'x'
+ // we want that to be a syntax error and not a literal x in literal quotation marks.
+ return "", fmt.Errorf("unquoted string cannot contain quote")
+ }
+ *s = AutoQuote(t)
+ return t, nil
+}
+
+type ErrorList []Error
+
+func (e ErrorList) Error() string {
+ errStrs := make([]string, len(e))
+ for i, err := range e {
+ errStrs[i] = err.Error()
+ }
+ return strings.Join(errStrs, "\n")
+}
+
+type Error struct {
+ Filename string
+ Pos Position
+ Verb string
+ ModPath string
+ Err error
+}
+
+func (e *Error) Error() string {
+ var pos string
+ if e.Pos.LineRune > 1 {
+ // Don't print LineRune if it's 1 (beginning of line).
+ // It's always 1 except in scanner errors, which are rare.
+ pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
+ } else if e.Pos.Line > 0 {
+ pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
+ } else if e.Filename != "" {
+ pos = fmt.Sprintf("%s: ", e.Filename)
+ }
+
+ var directive string
+ if e.ModPath != "" {
+ directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
+ }
+
+ return pos + directive + e.Err.Error()
+}
+
+func (e *Error) Unwrap() error { return e.Err }
+
+func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
+ t, err := parseString(s)
+ if err != nil {
+ return "", &Error{
+ Verb: verb,
+ ModPath: path,
+ Err: &module.InvalidVersionError{
+ Version: *s,
+ Err: err,
+ },
+ }
+ }
+ if fix != nil {
+ var err error
+ t, err = fix(path, t)
+ if err != nil {
+ if err, ok := err.(*module.ModuleError); ok {
+ return "", &Error{
+ Verb: verb,
+ ModPath: path,
+ Err: err.Err,
+ }
+ }
+ return "", err
+ }
+ }
+ if v := module.CanonicalVersion(t); v != "" {
+ *s = v
+ return *s, nil
+ }
+ return "", &Error{
+ Verb: verb,
+ ModPath: path,
+ Err: &module.InvalidVersionError{
+ Version: t,
+ Err: errors.New("must be of the form v1.2.3"),
+ },
+ }
+}
+
+func modulePathMajor(path string) (string, error) {
+ _, major, ok := module.SplitPathVersion(path)
+ if !ok {
+ return "", fmt.Errorf("invalid module path")
+ }
+ return major, nil
+}
+
+func (f *File) Format() ([]byte, error) {
+ return Format(f.Syntax), nil
+}
+
+// Cleanup cleans up the file f after any edit operations.
+// To avoid quadratic behavior, modifications like DropRequire
+// clear the entry but do not remove it from the slice.
+// Cleanup cleans out all the cleared entries.
+func (f *File) Cleanup() {
+ w := 0
+ for _, r := range f.Require {
+ if r.Mod.Path != "" {
+ f.Require[w] = r
+ w++
+ }
+ }
+ f.Require = f.Require[:w]
+
+ w = 0
+ for _, x := range f.Exclude {
+ if x.Mod.Path != "" {
+ f.Exclude[w] = x
+ w++
+ }
+ }
+ f.Exclude = f.Exclude[:w]
+
+ w = 0
+ for _, r := range f.Replace {
+ if r.Old.Path != "" {
+ f.Replace[w] = r
+ w++
+ }
+ }
+ f.Replace = f.Replace[:w]
+
+ f.Syntax.Cleanup()
+}
+
+func (f *File) AddGoStmt(version string) error {
+ if !GoVersionRE.MatchString(version) {
+ return fmt.Errorf("invalid language version string %q", version)
+ }
+ if f.Go == nil {
+ var hint Expr
+ if f.Module != nil && f.Module.Syntax != nil {
+ hint = f.Module.Syntax
+ }
+ f.Go = &Go{
+ Version: version,
+ Syntax: f.Syntax.addLine(hint, "go", version),
+ }
+ } else {
+ f.Go.Version = version
+ f.Syntax.updateLine(f.Go.Syntax, "go", version)
+ }
+ return nil
+}
+
+func (f *File) AddRequire(path, vers string) error {
+ need := true
+ for _, r := range f.Require {
+ if r.Mod.Path == path {
+ if need {
+ r.Mod.Version = vers
+ f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
+ need = false
+ } else {
+ f.Syntax.removeLine(r.Syntax)
+ *r = Require{}
+ }
+ }
+ }
+
+ if need {
+ f.AddNewRequire(path, vers, false)
+ }
+ return nil
+}
+
+func (f *File) AddNewRequire(path, vers string, indirect bool) {
+ line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
+ setIndirect(line, indirect)
+ f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
+}
+
+func (f *File) SetRequire(req []*Require) {
+ need := make(map[string]string)
+ indirect := make(map[string]bool)
+ for _, r := range req {
+ need[r.Mod.Path] = r.Mod.Version
+ indirect[r.Mod.Path] = r.Indirect
+ }
+
+ for _, r := range f.Require {
+ if v, ok := need[r.Mod.Path]; ok {
+ r.Mod.Version = v
+ r.Indirect = indirect[r.Mod.Path]
+ } else {
+ *r = Require{}
+ }
+ }
+
+ var newStmts []Expr
+ for _, stmt := range f.Syntax.Stmt {
+ switch stmt := stmt.(type) {
+ case *LineBlock:
+ if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
+ var newLines []*Line
+ for _, line := range stmt.Line {
+ if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
+ if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
+ line.Comments.Before = line.Comments.Before[:0]
+ }
+ line.Token[1] = need[p]
+ delete(need, p)
+ setIndirect(line, indirect[p])
+ newLines = append(newLines, line)
+ }
+ }
+ if len(newLines) == 0 {
+ continue // drop stmt
+ }
+ stmt.Line = newLines
+ }
+
+ case *Line:
+ if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
+ if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
+ stmt.Token[2] = need[p]
+ delete(need, p)
+ setIndirect(stmt, indirect[p])
+ } else {
+ continue // drop stmt
+ }
+ }
+ }
+ newStmts = append(newStmts, stmt)
+ }
+ f.Syntax.Stmt = newStmts
+
+ for path, vers := range need {
+ f.AddNewRequire(path, vers, indirect[path])
+ }
+ f.SortBlocks()
+}
+
+func (f *File) DropRequire(path string) error {
+ for _, r := range f.Require {
+ if r.Mod.Path == path {
+ f.Syntax.removeLine(r.Syntax)
+ *r = Require{}
+ }
+ }
+ return nil
+}
+
+func (f *File) AddExclude(path, vers string) error {
+ var hint *Line
+ for _, x := range f.Exclude {
+ if x.Mod.Path == path && x.Mod.Version == vers {
+ return nil
+ }
+ if x.Mod.Path == path {
+ hint = x.Syntax
+ }
+ }
+
+ f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
+ return nil
+}
+
+func (f *File) DropExclude(path, vers string) error {
+ for _, x := range f.Exclude {
+ if x.Mod.Path == path && x.Mod.Version == vers {
+ f.Syntax.removeLine(x.Syntax)
+ *x = Exclude{}
+ }
+ }
+ return nil
+}
+
+func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
+ need := true
+ old := module.Version{Path: oldPath, Version: oldVers}
+ new := module.Version{Path: newPath, Version: newVers}
+ tokens := []string{"replace", AutoQuote(oldPath)}
+ if oldVers != "" {
+ tokens = append(tokens, oldVers)
+ }
+ tokens = append(tokens, "=>", AutoQuote(newPath))
+ if newVers != "" {
+ tokens = append(tokens, newVers)
+ }
+
+ var hint *Line
+ for _, r := range f.Replace {
+ if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
+ if need {
+ // Found replacement for old; update to use new.
+ r.New = new
+ f.Syntax.updateLine(r.Syntax, tokens...)
+ need = false
+ continue
+ }
+ // Already added; delete other replacements for same.
+ f.Syntax.removeLine(r.Syntax)
+ *r = Replace{}
+ }
+ if r.Old.Path == oldPath {
+ hint = r.Syntax
+ }
+ }
+ if need {
+ f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
+ }
+ return nil
+}
+
+func (f *File) DropReplace(oldPath, oldVers string) error {
+ for _, r := range f.Replace {
+ if r.Old.Path == oldPath && r.Old.Version == oldVers {
+ f.Syntax.removeLine(r.Syntax)
+ *r = Replace{}
+ }
+ }
+ return nil
+}
+
+func (f *File) SortBlocks() {
+ f.removeDups() // otherwise sorting is unsafe
+
+ for _, stmt := range f.Syntax.Stmt {
+ block, ok := stmt.(*LineBlock)
+ if !ok {
+ continue
+ }
+ sort.Slice(block.Line, func(i, j int) bool {
+ li := block.Line[i]
+ lj := block.Line[j]
+ for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
+ if li.Token[k] != lj.Token[k] {
+ return li.Token[k] < lj.Token[k]
+ }
+ }
+ return len(li.Token) < len(lj.Token)
+ })
+ }
+}
+
+func (f *File) removeDups() {
+ have := make(map[module.Version]bool)
+ kill := make(map[*Line]bool)
+ for _, x := range f.Exclude {
+ if have[x.Mod] {
+ kill[x.Syntax] = true
+ continue
+ }
+ have[x.Mod] = true
+ }
+ var excl []*Exclude
+ for _, x := range f.Exclude {
+ if !kill[x.Syntax] {
+ excl = append(excl, x)
+ }
+ }
+ f.Exclude = excl
+
+ have = make(map[module.Version]bool)
+ // Later replacements take priority over earlier ones.
+ for i := len(f.Replace) - 1; i >= 0; i-- {
+ x := f.Replace[i]
+ if have[x.Old] {
+ kill[x.Syntax] = true
+ continue
+ }
+ have[x.Old] = true
+ }
+ var repl []*Replace
+ for _, x := range f.Replace {
+ if !kill[x.Syntax] {
+ repl = append(repl, x)
+ }
+ }
+ f.Replace = repl
+
+ var stmts []Expr
+ for _, stmt := range f.Syntax.Stmt {
+ switch stmt := stmt.(type) {
+ case *Line:
+ if kill[stmt] {
+ continue
+ }
+ case *LineBlock:
+ var lines []*Line
+ for _, line := range stmt.Line {
+ if !kill[line] {
+ lines = append(lines, line)
+ }
+ }
+ stmt.Line = lines
+ if len(lines) == 0 {
+ continue
+ }
+ }
+ stmts = append(stmts, stmt)
+ }
+ f.Syntax.Stmt = stmts
+}