kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/serving/tools/kwazthis/kwazthis.go (about)

     1  /*
     2   * Copyright 2015 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Binary kwazthis (K, what's this?) determines what references are located at a
    18  // particular offset (or line and column) within a file.  All results are
    19  // printed as JSON.
    20  //
    21  // By default, kwazthis will search for a .kythe configuration file in a
    22  // directory above the given --path (if it exists locally relative to the
    23  // current working directory).  If found, --path will be made relative to this
    24  // directory and --root before making any Kythe service requests.  If not found,
    25  // --path will be passed unchanged.  --ignore_local_repo will turn off this
    26  // behavior.
    27  //
    28  // Usage:
    29  //
    30  //	kwazthis --path kythe/cxx/tools/kindex_tool_main.cc --offset 2660
    31  //	kwazthis --path kythe/cxx/common/CommandLineUtils.cc --line 81 --column 27
    32  //	kwazthis --path kythe/java/com/google/devtools/kythe/analyzers/base/EntrySet.java --offset 2815
    33  package main
    34  
    35  import (
    36  	"context"
    37  	"encoding/json"
    38  	"flag"
    39  	"io/ioutil"
    40  	"os"
    41  	"path/filepath"
    42  	"strconv"
    43  	"strings"
    44  
    45  	"kythe.io/kythe/go/platform/vfs"
    46  	"kythe.io/kythe/go/services/graph"
    47  	"kythe.io/kythe/go/services/xrefs"
    48  	"kythe.io/kythe/go/serving/api"
    49  	"kythe.io/kythe/go/util/flagutil"
    50  	"kythe.io/kythe/go/util/kytheuri"
    51  	"kythe.io/kythe/go/util/log"
    52  	"kythe.io/kythe/go/util/schema"
    53  	"kythe.io/kythe/go/util/schema/edges"
    54  	"kythe.io/kythe/go/util/schema/facts"
    55  	"kythe.io/kythe/go/util/schema/tickets"
    56  
    57  	cpb "kythe.io/kythe/proto/common_go_proto"
    58  	gpb "kythe.io/kythe/proto/graph_go_proto"
    59  	spb "kythe.io/kythe/proto/storage_go_proto"
    60  	xpb "kythe.io/kythe/proto/xref_go_proto"
    61  )
    62  
    63  func init() {
    64  	flag.Usage = flagutil.SimpleUsage(`Determine what references are located at a particular offset (or line and column) within a file.
    65  
    66  kwazthis normally searches for a .kythe configuration file in a directory above
    67  the given --path (if it exists locally relative to the current working
    68  directory).  If found, --path will be made relative to this directory and --root
    69  before making any Kythe service requests.  If not found, --path will be passed
    70  unchanged.
    71  
    72  If the given --path file is found locally and --dirty_buffer is unset,
    73  --dirty_buffer is automatically set to found local file and sent to the server .
    74  
    75  --local_repo supplies kwazthis with the corpus root without searching the
    76  filesystem for the .kythe file and --local_repo=NONE will turn off all local
    77  filesystem behavior completely (including the automatic --dirty_buffer
    78  feature).`,
    79  		`(--offset int | --line int --column int) (--path p | --signature s)
    80  [--corpus c] [--root r] [--language l]
    81  [--api spec] [--local_repo root] [--dirty_buffer path] [--skip_defs]`)
    82  }
    83  
    84  var (
    85  	ctx = context.Background()
    86  
    87  	apiFlag = api.Flag("api", api.CommonDefault, api.CommonFlagUsage)
    88  
    89  	localRepoRoot = flag.String("local_repo", "",
    90  		`Path to local repository root ("" indicates to search for a .kythe configuration file in a directory about the given --path; "NONE" completely disables all local repository behavior)`)
    91  
    92  	dirtyBuffer = flag.String("dirty_buffer", "", "Path to file with dirty buffer contents (optional)")
    93  
    94  	path   = flag.String("path", "", "Path of file")
    95  	corpus = flag.String("corpus", "", "Corpus of file VName")
    96  	root   = flag.String("root", "", "Root of file VName")
    97  
    98  	offset       = flag.Int("offset", -1, "Non-negative offset in file to list references (mutually exclusive with --line and --column)")
    99  	lineNumber   = flag.Int("line", -1, "1-based line number in file to list references (must be given with --column)")
   100  	columnOffset = flag.Int("column", -1, "Non-negative column offset in file to list references (must be given with --line)")
   101  
   102  	skipDefinitions = flag.Bool("skip_defs", false, "Skip listing definitions for each node")
   103  )
   104  
   105  var (
   106  	xs xrefs.Service
   107  	gs graph.Service
   108  )
   109  
   110  type definition struct {
   111  	File  *spb.VName `json:"file"`
   112  	Start int        `json:"start"`
   113  	End   int        `json:"end"`
   114  }
   115  
   116  type reference struct {
   117  	Span struct {
   118  		Start int    `json:"start"`
   119  		End   int    `json:"end"`
   120  		Text  string `json:"text,omitempty"`
   121  	} `json:"span"`
   122  	Kind string `json:"kind"`
   123  
   124  	Node struct {
   125  		Ticket  string   `json:"ticket"`
   126  		Names   []string `json:"names,omitempty"`
   127  		Kind    string   `json:"kind,omitempty"`
   128  		Subkind string   `json:"subkind,omitempty"`
   129  		Typed   string   `json:"typed,omitempty"`
   130  
   131  		Definitions []*definition `json:"definitions,omitempty"`
   132  	} `json:"node"`
   133  }
   134  
   135  var (
   136  	definedAtEdge        = edges.Mirror(edges.Defines)
   137  	definedBindingAtEdge = edges.Mirror(edges.DefinesBinding)
   138  )
   139  
   140  func main() {
   141  	flag.Parse()
   142  	if flag.NArg() > 0 {
   143  		flagutil.UsageErrorf("unknown non-flag argument(s): %v", flag.Args())
   144  	} else if *offset < 0 && (*lineNumber < 0 || *columnOffset < 0) {
   145  		flagutil.UsageError("non-negative --offset (or --line and --column) required")
   146  	} else if *path == "" {
   147  		flagutil.UsageError("must provide --path")
   148  	}
   149  
   150  	defer (*apiFlag).Close(ctx)
   151  	xs = *apiFlag
   152  	gs = *apiFlag
   153  
   154  	relPath := *path
   155  	if *localRepoRoot != "NONE" {
   156  		if _, err := os.Stat(relPath); err == nil {
   157  			absPath, err := filepath.Abs(relPath)
   158  			if err != nil {
   159  				log.Fatal(err)
   160  			}
   161  			if *dirtyBuffer == "" {
   162  				*dirtyBuffer = absPath
   163  			}
   164  
   165  			kytheRoot := *localRepoRoot
   166  			if kytheRoot == "" {
   167  				kytheRoot = findKytheRoot(filepath.Dir(absPath))
   168  			}
   169  			if kytheRoot != "" {
   170  				relPath, err = filepath.Rel(filepath.Join(kytheRoot, *root), absPath)
   171  				if err != nil {
   172  					log.Fatal(err)
   173  				}
   174  			}
   175  		}
   176  	}
   177  
   178  	fileTicket := (&kytheuri.URI{Corpus: *corpus, Root: *root, Path: relPath}).String()
   179  	point := &cpb.Point{
   180  		ByteOffset:   int32(*offset),
   181  		LineNumber:   int32(*lineNumber),
   182  		ColumnOffset: int32(*columnOffset),
   183  	}
   184  	dirtyBuffer := readDirtyBuffer(ctx)
   185  	decor, err := xs.Decorations(ctx, &xpb.DecorationsRequest{
   186  		Location: &xpb.Location{
   187  			Ticket: fileTicket,
   188  			Kind:   xpb.Location_SPAN,
   189  			Span:   &cpb.Span{Start: point, End: point},
   190  		},
   191  		SpanKind:    xpb.DecorationsRequest_AROUND_SPAN,
   192  		References:  true,
   193  		SourceText:  true,
   194  		DirtyBuffer: dirtyBuffer,
   195  		Filter: []string{
   196  			facts.NodeKind,
   197  			facts.Subkind,
   198  		},
   199  	})
   200  	if err != nil {
   201  		log.Fatal(err)
   202  	}
   203  	nodes := graph.NodesMap(decor.Nodes)
   204  
   205  	en := json.NewEncoder(os.Stdout)
   206  	for _, ref := range decor.Reference {
   207  		start, end := int(ref.Span.Start.ByteOffset), int(ref.Span.End.ByteOffset)
   208  
   209  		var r reference
   210  		r.Span.Start = start
   211  		r.Span.End = end
   212  		if len(dirtyBuffer) > 0 {
   213  			r.Span.Text = string(dirtyBuffer[start:end])
   214  		} // TODO(schroederc): add option to get anchor text from DecorationsReply
   215  		r.Kind = strings.TrimPrefix(ref.Kind, edges.Prefix)
   216  		r.Node.Ticket = ref.TargetTicket
   217  
   218  		node := nodes[ref.TargetTicket]
   219  		r.Node.Kind = string(node[facts.NodeKind])
   220  		r.Node.Subkind = string(node[facts.Subkind])
   221  
   222  		// TODO(schroederc): use CrossReferences method
   223  		if eReply, err := graph.AllEdges(ctx, gs, &gpb.EdgesRequest{
   224  			Ticket: []string{ref.TargetTicket},
   225  			Kind:   []string{edges.Named, edges.Typed, definedAtEdge, definedBindingAtEdge},
   226  		}); err != nil {
   227  			log.Warningf("error getting edges for %q: %v", ref.TargetTicket, err)
   228  		} else {
   229  			matching := graph.EdgesMap(eReply.EdgeSets)[ref.TargetTicket]
   230  			for name := range matching[edges.Named] {
   231  				if uri, err := kytheuri.Parse(name); err != nil {
   232  					log.Warningf("named node ticket (%q) could not be parsed: %v", name, err)
   233  				} else {
   234  					r.Node.Names = append(r.Node.Names, uri.Signature)
   235  				}
   236  			}
   237  
   238  			for typed := range matching[edges.Typed] {
   239  				r.Node.Typed = typed
   240  				break
   241  			}
   242  
   243  			if !*skipDefinitions {
   244  				defs := matching[definedAtEdge]
   245  				if len(defs) == 0 {
   246  					defs = matching[definedBindingAtEdge]
   247  				}
   248  				for defAnchor := range defs {
   249  					def, err := completeDefinition(defAnchor)
   250  					if err != nil {
   251  						log.Warningf("failed to complete definition for %q: %v", defAnchor, err)
   252  					} else {
   253  						r.Node.Definitions = append(r.Node.Definitions, def)
   254  					}
   255  				}
   256  			}
   257  		}
   258  
   259  		if err := en.Encode(r); err != nil {
   260  			log.Fatal(err)
   261  		}
   262  	}
   263  }
   264  
   265  func completeDefinition(defAnchor string) (*definition, error) {
   266  	parentFile, err := tickets.AnchorFile(defAnchor)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	parent, err := kytheuri.Parse(parentFile)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	locReply, err := gs.Nodes(ctx, &gpb.NodesRequest{
   275  		Ticket: []string{defAnchor},
   276  		Filter: []string{schema.AnchorLocFilter},
   277  	})
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	nodes := graph.NodesMap(locReply.Nodes)
   282  	start, end := parseAnchorSpan(nodes[defAnchor])
   283  	return &definition{
   284  		File:  parent.VName(),
   285  		Start: start,
   286  		End:   end,
   287  	}, nil
   288  }
   289  
   290  func parseAnchorSpan(anchor map[string][]byte) (start int, end int) {
   291  	start, _ = strconv.Atoi(string(anchor[facts.AnchorStart]))
   292  	end, _ = strconv.Atoi(string(anchor[facts.AnchorEnd]))
   293  	return
   294  }
   295  
   296  func readDirtyBuffer(ctx context.Context) []byte {
   297  	if *dirtyBuffer == "" {
   298  		return nil
   299  	}
   300  
   301  	f, err := vfs.Open(ctx, *dirtyBuffer)
   302  	if err != nil {
   303  		log.Fatalf("ERROR: could not open dirty buffer at %q: %v", *dirtyBuffer, err)
   304  	}
   305  	defer f.Close()
   306  	data, err := ioutil.ReadAll(f)
   307  	if err != nil {
   308  		log.Fatalf("ERROR: could read dirty buffer at %q: %v", *dirtyBuffer, err)
   309  	}
   310  	return data
   311  }
   312  
   313  func findKytheRoot(dir string) string {
   314  	for {
   315  		if fi, err := os.Stat(filepath.Join(dir, ".kythe")); err == nil && fi.Mode().IsRegular() {
   316  			return dir
   317  		}
   318  		if dir == "/" {
   319  			break
   320  		}
   321  		dir = filepath.Dir(dir)
   322  	}
   323  	return ""
   324  }