1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7// This file defines utilities for working with file positions.
8
9import (
10	"fmt"
11	"go/build"
12	"go/parser"
13	"go/token"
14	"os"
15	"path/filepath"
16	"strconv"
17	"strings"
18
19	"golang.org/x/tools/go/ast/astutil"
20	"golang.org/x/tools/go/buildutil"
21)
22
23// parseOctothorpDecimal returns the numeric value if s matches "#%d",
24// otherwise -1.
25func parseOctothorpDecimal(s string) int {
26	if s != "" && s[0] == '#' {
27		if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil {
28			return int(s)
29		}
30	}
31	return -1
32}
33
34// parsePos parses a string of the form "file:pos" or
35// file:start,end" where pos, start, end match #%d and represent byte
36// offsets, and returns its components.
37//
38// (Numbers without a '#' prefix are reserved for future use,
39// e.g. to indicate line/column positions.)
40//
41func parsePos(pos string) (filename string, startOffset, endOffset int, err error) {
42	if pos == "" {
43		err = fmt.Errorf("no source position specified")
44		return
45	}
46
47	colon := strings.LastIndex(pos, ":")
48	if colon < 0 {
49		err = fmt.Errorf("bad position syntax %q", pos)
50		return
51	}
52	filename, offset := pos[:colon], pos[colon+1:]
53	startOffset = -1
54	endOffset = -1
55	if comma := strings.Index(offset, ","); comma < 0 {
56		// e.g. "foo.go:#123"
57		startOffset = parseOctothorpDecimal(offset)
58		endOffset = startOffset
59	} else {
60		// e.g. "foo.go:#123,#456"
61		startOffset = parseOctothorpDecimal(offset[:comma])
62		endOffset = parseOctothorpDecimal(offset[comma+1:])
63	}
64	if startOffset < 0 || endOffset < 0 {
65		err = fmt.Errorf("invalid offset %q in query position", offset)
66		return
67	}
68	return
69}
70
71// fileOffsetToPos translates the specified file-relative byte offsets
72// into token.Pos form.  It returns an error if the file was not found
73// or the offsets were out of bounds.
74//
75func fileOffsetToPos(file *token.File, startOffset, endOffset int) (start, end token.Pos, err error) {
76	// Range check [start..end], inclusive of both end-points.
77
78	if 0 <= startOffset && startOffset <= file.Size() {
79		start = file.Pos(int(startOffset))
80	} else {
81		err = fmt.Errorf("start position is beyond end of file")
82		return
83	}
84
85	if 0 <= endOffset && endOffset <= file.Size() {
86		end = file.Pos(int(endOffset))
87	} else {
88		err = fmt.Errorf("end position is beyond end of file")
89		return
90	}
91
92	return
93}
94
95// sameFile returns true if x and y have the same basename and denote
96// the same file.
97//
98func sameFile(x, y string) bool {
99	if filepath.Base(x) == filepath.Base(y) { // (optimisation)
100		if xi, err := os.Stat(x); err == nil {
101			if yi, err := os.Stat(y); err == nil {
102				return os.SameFile(xi, yi)
103			}
104		}
105	}
106	return false
107}
108
109// fastQueryPos parses the position string and returns a queryPos.
110// It parses only a single file and does not run the type checker.
111func fastQueryPos(ctxt *build.Context, pos string) (*queryPos, error) {
112	filename, startOffset, endOffset, err := parsePos(pos)
113	if err != nil {
114		return nil, err
115	}
116
117	// Parse the file, opening it the file via the build.Context
118	// so that we observe the effects of the -modified flag.
119	fset := token.NewFileSet()
120	cwd, _ := os.Getwd()
121	f, err := buildutil.ParseFile(fset, ctxt, nil, cwd, filename, parser.Mode(0))
122	// ParseFile usually returns a partial file along with an error.
123	// Only fail if there is no file.
124	if f == nil {
125		return nil, err
126	}
127	if !f.Pos().IsValid() {
128		return nil, fmt.Errorf("%s is not a Go source file", filename)
129	}
130
131	start, end, err := fileOffsetToPos(fset.File(f.Pos()), startOffset, endOffset)
132	if err != nil {
133		return nil, err
134	}
135
136	path, exact := astutil.PathEnclosingInterval(f, start, end)
137	if path == nil {
138		return nil, fmt.Errorf("no syntax here")
139	}
140
141	return &queryPos{fset, start, end, path, exact, nil}, nil
142}
143