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 }