Example #1
0
File: ktags.go Project: jwatt/kythe
func getTagFields(xs xrefs.Service, ticket string) ([]string, error) {
	reply, err := xs.Edges(ctx, &xpb.EdgesRequest{
		Ticket: []string{ticket},
		Kind:   []string{schema.ChildOfEdge, schema.ParamEdge},
		Filter: []string{schema.NodeKindFact, schema.SubkindFact, identifierFact},
	})
	if err != nil || len(reply.EdgeSet) == 0 {
		return nil, err
	}

	var fields []string

	nodes := xrefs.NodesMap(reply.Node)
	edges := xrefs.EdgesMap(reply.EdgeSet)

	switch string(nodes[ticket][schema.NodeKindFact]) + "|" + string(nodes[ticket][schema.SubkindFact]) {
	case schema.FunctionKind + "|":
		fields = append(fields, "f")
		fields = append(fields, "arity:"+strconv.Itoa(len(edges[ticket][schema.ParamEdge])))
	case schema.EnumKind + "|" + schema.EnumClassSubkind:
		fields = append(fields, "g")
	case schema.PackageKind + "|":
		fields = append(fields, "p")
	case schema.RecordKind + "|" + schema.ClassSubkind:
		fields = append(fields, "c")
	case schema.VariableKind + "|":
		fields = append(fields, "v")
	}

	for _, parent := range edges[ticket][schema.ChildOfEdge] {
		parentIdent := string(nodes[parent][identifierFact])
		if parentIdent == "" {
			continue
		}
		switch string(nodes[parent][schema.NodeKindFact]) + "|" + string(nodes[parent][schema.SubkindFact]) {
		case schema.FunctionKind + "|":
			fields = append(fields, "function:"+parentIdent)
		case schema.RecordKind + "|" + schema.ClassSubkind:
			fields = append(fields, "class:"+parentIdent)
		case schema.EnumKind + "|" + schema.EnumClassSubkind:
			fields = append(fields, "enum:"+parentIdent)
		}
	}

	return fields, nil
}
Example #2
0
func getTagFields(xs xrefs.Service, ticket string) ([]string, error) {
	reply, err := xs.Edges(ctx, &gpb.EdgesRequest{
		Ticket: []string{ticket},
		Kind:   []string{edges.ChildOf, edges.Param},
		Filter: []string{facts.NodeKind, facts.Subkind, identifierFact},
	})
	if err != nil || len(reply.EdgeSets) == 0 {
		return nil, err
	}

	var fields []string

	nmap := xrefs.NodesMap(reply.Nodes)
	emap := xrefs.EdgesMap(reply.EdgeSets)

	switch string(nmap[ticket][facts.NodeKind]) + "|" + string(nmap[ticket][facts.Subkind]) {
	case nodes.Function + "|":
		fields = append(fields, "f")
		fields = append(fields, "arity:"+strconv.Itoa(len(emap[ticket][edges.Param])))
	case nodes.Enum + "|" + nodes.EnumClass:
		fields = append(fields, "g")
	case nodes.Package + "|":
		fields = append(fields, "p")
	case nodes.Record + "|" + nodes.Class:
		fields = append(fields, "c")
	case nodes.Variable + "|":
		fields = append(fields, "v")
	}

	for parent := range emap[ticket][edges.ChildOf] {
		parentIdent := string(nmap[parent][identifierFact])
		if parentIdent == "" {
			continue
		}
		switch string(nmap[parent][facts.NodeKind]) + "|" + string(nmap[parent][facts.Subkind]) {
		case nodes.Function + "|":
			fields = append(fields, "function:"+parentIdent)
		case nodes.Record + "|" + nodes.Class:
			fields = append(fields, "class:"+parentIdent)
		case nodes.Enum + "|" + nodes.EnumClass:
			fields = append(fields, "enum:"+parentIdent)
		}
	}

	return fields, nil
}
Example #3
0
var (
	ctx = context.Background()

	xs  xrefs.Service
	ft  filetree.Service
	idx search.Service

	// ls flags
	lsURIs    bool
	filesOnly bool
	dirsOnly  bool

	// node flags
	nodeFilters       string
	factSizeThreshold int

	// edges flags
	countOnly   bool
	targetsOnly bool
	edgeKinds   string
	pageToken   string
	pageSize    int

	// source/decor flags
	decorSpan string

	// decor flags
	dirtyFile string
	refFormat string

	// xrefs flags
	defKind, refKind, docKind string
	relatedNodes              bool

	// search flags
	suffixWildcard string
	corpus         string
	root           string
	path           string
	language       string
	signature      string

	spanHelp = `Limit results to this span (e.g. "10-30", "b1462-b1847", "3:5-3:10")
      Formats:
        b\d+-b\d+             -- Byte-offsets
        \d+(:\d+)?-\d+(:\d+)? -- Line offsets with optional column offsets`

	cmdLS = newCommand("ls", "[--uris] [directory-uri]",
		"List a directory's contents",
		func(flag *flag.FlagSet) {
			flag.BoolVar(&lsURIs, "uris", false, "Display files/directories as Kythe URIs")
			flag.BoolVar(&filesOnly, "files", false, "Display only files")
			flag.BoolVar(&dirsOnly, "dirs", false, "Display only directories")
		},
		func(flag *flag.FlagSet) error {
			if filesOnly && dirsOnly {
				return errors.New("--files and --dirs are mutually exclusive")
			}

			if len(flag.Args()) == 0 {
				req := &ftpb.CorpusRootsRequest{}
				logRequest(req)
				cr, err := ft.CorpusRoots(ctx, req)
				if err != nil {
					return err
				}
				return displayCorpusRoots(cr)
			}
			var corpus, root, path string
			switch len(flag.Args()) {
			case 1:
				uri, err := kytheuri.Parse(flag.Arg(0))
				if err != nil {
					log.Fatalf("invalid uri %q: %v", flag.Arg(0), err)
				}
				corpus = uri.Corpus
				root = uri.Root
				path = uri.Path
			default:
				flag.Usage()
				os.Exit(1)
			}
			path = filepath.Join("/", path)
			req := &ftpb.DirectoryRequest{
				Corpus: corpus,
				Root:   root,
				Path:   path,
			}
			logRequest(req)
			dir, err := ft.Directory(ctx, req)
			if err != nil {
				return err
			} else if dir == nil {
				return fmt.Errorf("no such directory: %q in corpus %q (root %q)", path, corpus, root)
			}

			if filesOnly {
				dir.Subdirectory = nil
			} else if dirsOnly {
				dir.File = nil
			}

			return displayDirectory(dir)
		})

	cmdEdges = newCommand("edges", "[--count_only] [--kinds edgeKind1,edgeKind2,...] [--page_token token] [--page_size num] <ticket>",
		"Retrieve outward edges from a node",
		func(flag *flag.FlagSet) {
			flag.BoolVar(&countOnly, "count_only", false, "Only print counts per edge kind")
			flag.BoolVar(&targetsOnly, "targets_only", false, "Only display edge targets")
			flag.StringVar(&edgeKinds, "kinds", "", "Comma-separated list of edge kinds to return (default returns all)")
			flag.StringVar(&pageToken, "page_token", "", "Edges page token")
			flag.IntVar(&pageSize, "page_size", 0, "Maximum number of edges returned (0 lets the service use a sensible default)")
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.EdgesRequest{
				Ticket:    flag.Args(),
				PageToken: pageToken,
				PageSize:  int32(pageSize),
			}
			if edgeKinds != "" {
				req.Kind = strings.Split(edgeKinds, ",")
			}
			logRequest(req)
			reply, err := xs.Edges(ctx, req)
			if err != nil {
				return err
			}
			if reply.NextPageToken != "" {
				defer log.Printf("Next page token: %s", reply.NextPageToken)
			}
			if countOnly {
				return displayEdgeCounts(reply)
			} else if targetsOnly {
				return displayTargets(reply.EdgeSet)
			}
			return displayEdges(reply)
		})

	cmdXRefs = newCommand("xrefs", "[--definitions kind] [--references kind] [--documentation kind] [--related_nodes] [--page_token token] [--page_size num] <ticket>",
		"Retrieve the global cross-references of the given node",
		func(flag *flag.FlagSet) {
			flag.StringVar(&defKind, "definitions", "all", "Kind of definitions to return (kinds: all, binding, full, or none)")
			flag.StringVar(&refKind, "references", "all", "Kind of references to return (kinds: all or none)")
			flag.StringVar(&docKind, "documentation", "all", "Kind of documentation to return (kinds: all or none)")
			flag.BoolVar(&relatedNodes, "related_nodes", false, "Whether to request related nodes")

			flag.StringVar(&pageToken, "page_token", "", "CrossReferences page token")
			flag.IntVar(&pageSize, "page_size", 0, "Maximum number of cross-references returned (0 lets the service use a sensible default)")
		},
		func(flag *flag.FlagSet) error {
			log.Println("WARNING: this API is currently experimental")

			req := &xpb.CrossReferencesRequest{
				Ticket:    flag.Args(),
				PageToken: pageToken,
				PageSize:  int32(pageSize),
			}
			if relatedNodes {
				req.Filter = []string{schema.NodeKindFact}
			}
			switch defKind {
			case "all":
				req.DefinitionKind = xpb.CrossReferencesRequest_ALL_DEFINITIONS
			case "none":
				req.DefinitionKind = xpb.CrossReferencesRequest_NO_DEFINITIONS
			case "binding":
				req.DefinitionKind = xpb.CrossReferencesRequest_BINDING_DEFINITIONS
			case "full":
				req.DefinitionKind = xpb.CrossReferencesRequest_FULL_DEFINITIONS
			default:
				return fmt.Errorf("unknown definition kind: %q", defKind)
			}
			switch refKind {
			case "all":
				req.ReferenceKind = xpb.CrossReferencesRequest_ALL_REFERENCES
			case "none":
				req.ReferenceKind = xpb.CrossReferencesRequest_NO_REFERENCES
			default:
				return fmt.Errorf("unknown reference kind: %q", refKind)
			}
			switch docKind {
			case "all":
				req.DocumentationKind = xpb.CrossReferencesRequest_ALL_DOCUMENTATION
			case "none":
				req.DocumentationKind = xpb.CrossReferencesRequest_NO_DOCUMENTATION
			default:
				return fmt.Errorf("unknown documentation kind: %q", docKind)
			}
			logRequest(req)
			reply, err := xs.CrossReferences(ctx, req)
			if err != nil {
				return err
			}
			if reply.NextPageToken != "" {
				defer log.Printf("Next page token: %s", reply.NextPageToken)
			}
			return displayXRefs(reply)
		})

	cmdNode = newCommand("node", "[--filters factFilter1,factFilter2,...] [--max_fact_size] <ticket>",
		"Retrieve a node's facts",
		func(flag *flag.FlagSet) {
			flag.StringVar(&nodeFilters, "filters", "", "Comma-separated list of node fact filters (default returns all)")
			flag.IntVar(&factSizeThreshold, "max_fact_size", 64,
				"Maximum size of fact values to display.  Facts with byte lengths longer than this value will only have their fact names displayed.")
		},
		func(flag *flag.FlagSet) error {
			if factSizeThreshold < 0 {
				return fmt.Errorf("invalid --max_fact_size value (must be non-negative): %d", factSizeThreshold)
			}

			req := &xpb.NodesRequest{
				Ticket: flag.Args(),
			}
			if nodeFilters != "" {
				req.Filter = strings.Split(nodeFilters, ",")
			}
			logRequest(req)
			reply, err := xs.Nodes(ctx, req)
			if err != nil {
				return err
			}
			return displayNodes(reply.Node)
		})

	cmdSource = newCommand("source", "[--span span] <file-ticket>",
		"Retrieve a file's source text",
		func(flag *flag.FlagSet) {
			flag.StringVar(&decorSpan, "span", "", spanHelp)
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.DecorationsRequest{
				Location: &xpb.Location{
					Ticket: flag.Arg(0),
				},
				SourceText: true,
			}
			if decorSpan != "" {
				start, end, err := parseSpan(decorSpan)
				if err != nil {
					return fmt.Errorf("invalid --span %q: %v", decorSpan, err)
				}

				req.Location.Kind = xpb.Location_SPAN
				req.Location.Start = start
				req.Location.End = end
			}

			logRequest(req)
			reply, err := xs.Decorations(ctx, req)
			if err != nil {
				return err
			}
			return displaySource(reply)
		})

	cmdDecor = newCommand("decor", "[--format spec] [--dirty file] [--span span] <file-ticket>",
		"List a file's anchor decorations",
		func(flag *flag.FlagSet) {
			// TODO(schroederc): add option to look for dirty files based on file-ticket path and a directory root
			flag.StringVar(&dirtyFile, "dirty", "", "Send the given file as the dirty buffer for patching references")
			flag.StringVar(&refFormat, "format", "@edgeKind@\t@^line@:@^col@-@$line@:@$col@\t@nodeKind@\t@target@",
				`Format for each decoration result.
      Format Markers:
        @source@   -- ticket of anchor source node
        @target@   -- ticket of referenced target node
        @edgeKind@ -- edge kind from anchor node to its referenced target
        @nodeKind@ -- node kind of referenced target
        @subkind@  -- subkind of referenced target
        @^offset@  -- anchor source's starting byte-offset
        @^line@    -- anchor source's starting line
        @^col@     -- anchor source's starting column offset
        @$offset@  -- anchor source's ending byte-offset
        @$line@    -- anchor source's ending line
        @$col@     -- anchor source's ending column offset`)
			flag.StringVar(&decorSpan, "span", "", spanHelp)
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.DecorationsRequest{
				Location: &xpb.Location{
					Ticket: flag.Arg(0),
				},
				References: true,
				Filter: []string{
					schema.NodeKindFact,
					schema.SubkindFact,
					schema.AnchorLocFilter, // TODO(schroederc): remove this backwards-compatibility fix
				},
			}
			if dirtyFile != "" {
				f, err := vfs.Open(ctx, dirtyFile)
				if err != nil {
					return fmt.Errorf("error opening dirty buffer file at %q: %v", dirtyFile, err)
				}
				buf, err := ioutil.ReadAll(f)
				if err != nil {
					f.Close()
					return fmt.Errorf("error reading dirty buffer file: %v", err)
				} else if err := f.Close(); err != nil {
					return fmt.Errorf("error closing dirty buffer file: %v", err)
				}
				req.DirtyBuffer = buf
			}
			if decorSpan != "" {
				start, end, err := parseSpan(decorSpan)
				if err != nil {
					return fmt.Errorf("invalid --span %q: %v", decorSpan, err)
				}

				req.Location.Kind = xpb.Location_SPAN
				req.Location.Start = start
				req.Location.End = end
			} else {
				req.SourceText = true // TODO(schroederc): remove need for this
			}

			logRequest(req)
			reply, err := xs.Decorations(ctx, req)
			if err != nil {
				return err
			}

			if !req.SourceText {
				// We need to grab the full SourceText to normalize each anchor's
				// location, but when given a --span, we don't receive the full text and
				// we require a separate Nodes call.
				// TODO(schroederc): add Locations for each DecorationsReply_Reference

				nodesReq := &xpb.NodesRequest{
					Ticket: []string{req.Location.Ticket},
					Filter: []string{schema.TextFact, schema.TextEncodingFact},
				}
				logRequest(nodesReq)
				fileNodeReply, err := xs.Nodes(ctx, nodesReq)
				if err != nil {
					return err
				}
				for _, n := range fileNodeReply.Node {
					if n.Ticket != req.Location.Ticket {
						log.Printf("WARNING: received unrequested node: %q", n.Ticket)
						continue
					}
					for _, f := range n.Fact {
						switch f.Name {
						case schema.TextFact:
							reply.SourceText = f.Value
						case schema.TextEncodingFact:
							reply.Encoding = string(f.Value)
						}
					}
				}
			}

			return displayDecorations(req.DirtyBuffer, reply)
		})

	cmdSearch = newCommand("search", "[--corpus c] [--sig s] [--root r] [--lang l] [--path p] [factName factValue]...",
		"Search for nodes based on partial components and fact values.",
		func(flag *flag.FlagSet) {
			flag.StringVar(&suffixWildcard, "suffix_wildcard", "%", "Suffix wildcard for search values (optional)")
			flag.StringVar(&corpus, "corpus", "", "Limit results to nodes with the given corpus (optional)")
			flag.StringVar(&root, "root", "", "Limit results to nodes with the given root (optional)")
			flag.StringVar(&path, "path", "", "Limit results to nodes with the given path (optional)")
			flag.StringVar(&signature, "sig", "", "Limit results to nodes with the given signature (optional)")
			flag.StringVar(&language, "lang", "", "Limit results to nodes with the given language (optional)")
		},
		func(flag *flag.FlagSet) error {
			if len(flag.Args())%2 != 0 {
				return fmt.Errorf("given odd number of arguments (%d): %v", len(flag.Args()), flag.Args())
			}

			req := &spb.SearchRequest{
				Partial: &spb.VName{
					Corpus:    strings.TrimSuffix(corpus, suffixWildcard),
					Signature: strings.TrimSuffix(signature, suffixWildcard),
					Root:      strings.TrimSuffix(root, suffixWildcard),
					Path:      strings.TrimSuffix(path, suffixWildcard),
					Language:  strings.TrimSuffix(language, suffixWildcard),
				},
			}
			req.PartialPrefix = &spb.VNameMask{
				Corpus:    req.Partial.Corpus != corpus,
				Signature: req.Partial.Signature != signature,
				Root:      req.Partial.Root != root,
				Path:      req.Partial.Path != path,
				Language:  req.Partial.Language != language,
			}
			for i := 0; i < len(flag.Args()); i = i + 2 {
				if flag.Arg(i) == schema.TextFact {
					log.Printf("WARNING: Large facts such as %s are not likely to be indexed", schema.TextFact)
				}
				v := strings.TrimSuffix(flag.Arg(i+1), suffixWildcard)
				req.Fact = append(req.Fact, &spb.SearchRequest_Fact{
					Name:   flag.Arg(i),
					Value:  []byte(v),
					Prefix: v != flag.Arg(i+1),
				})
			}

			logRequest(req)
			reply, err := idx.Search(ctx, req)
			if err != nil {
				return err
			}
			return displaySearch(reply)
		})
)
Example #4
0
var (
	ctx = context.Background()

	xs xrefs.Service
	ft filetree.Service

	// ls flags
	lsURIs    bool
	filesOnly bool
	dirsOnly  bool

	// node flags
	nodeFilters       string
	factSizeThreshold int

	// edges flags
	dotGraph    bool
	countOnly   bool
	targetsOnly bool
	edgeKinds   string
	pageToken   string
	pageSize    int

	// callers flags
	includeOverrides bool

	// docs flags

	// source/decor flags
	decorSpan string

	// decor flags
	targetDefs bool
	dirtyFile  string
	refFormat  string

	// xrefs flags
	defKind, declKind, refKind, docKind string
	relatedNodes                        bool

	spanHelp = `Limit results to this span (e.g. "10-30", "b1462-b1847", "3:5-3:10")
      Formats:
        b\d+-b\d+             -- Byte-offsets
        \d+(:\d+)?-\d+(:\d+)? -- Line offsets with optional column offsets`

	cmdLS = newCommand("ls", "[--uris] [directory-uri]",
		"List a directory's contents",
		func(flag *flag.FlagSet) {
			flag.BoolVar(&lsURIs, "uris", false, "Display files/directories as Kythe URIs")
			flag.BoolVar(&filesOnly, "files", false, "Display only files")
			flag.BoolVar(&dirsOnly, "dirs", false, "Display only directories")
		},
		func(flag *flag.FlagSet) error {
			if filesOnly && dirsOnly {
				return errors.New("--files and --dirs are mutually exclusive")
			}

			if len(flag.Args()) == 0 {
				req := &ftpb.CorpusRootsRequest{}
				logRequest(req)
				cr, err := ft.CorpusRoots(ctx, req)
				if err != nil {
					return err
				}
				return displayCorpusRoots(cr)
			}
			var corpus, root, path string
			switch len(flag.Args()) {
			case 1:
				uri, err := kytheuri.Parse(flag.Arg(0))
				if err != nil {
					log.Fatalf("invalid uri %q: %v", flag.Arg(0), err)
				}
				corpus = uri.Corpus
				root = uri.Root
				path = uri.Path
			default:
				flag.Usage()
				os.Exit(1)
			}
			path = filetree.CleanDirPath(path)
			req := &ftpb.DirectoryRequest{
				Corpus: corpus,
				Root:   root,
				Path:   path,
			}
			logRequest(req)
			dir, err := ft.Directory(ctx, req)
			if err != nil {
				return err
			}

			if filesOnly {
				dir.Subdirectory = nil
			} else if dirsOnly {
				dir.File = nil
			}

			return displayDirectory(dir)
		})

	cmdEdges = newCommand("edges", "[--count_only | --targets_only | --graphvizviz] [--kinds edgeKind1,edgeKind2,...] [--page_token token] [--page_size num] <ticket>",
		"Retrieve outward edges from a node",
		func(flag *flag.FlagSet) {
			flag.BoolVar(&dotGraph, "graphviz", false, "Print resulting edges as a dot graph")
			flag.BoolVar(&countOnly, "count_only", false, "Only print counts per edge kind")
			flag.BoolVar(&targetsOnly, "targets_only", false, "Only display edge targets")
			flag.StringVar(&edgeKinds, "kinds", "", "Comma-separated list of edge kinds to return (default returns all)")
			flag.StringVar(&pageToken, "page_token", "", "Edges page token")
			flag.IntVar(&pageSize, "page_size", 0, "Maximum number of edges returned (0 lets the service use a sensible default)")
		},
		func(flag *flag.FlagSet) error {
			if countOnly && targetsOnly {
				return errors.New("--count_only and --targets_only are mutually exclusive")
			} else if countOnly && dotGraph {
				return errors.New("--count_only and --graphviz are mutually exclusive")
			} else if targetsOnly && dotGraph {
				return errors.New("--targets_only and --graphviz are mutually exclusive")
			}

			req := &xpb.EdgesRequest{
				Ticket:    flag.Args(),
				PageToken: pageToken,
				PageSize:  int32(pageSize),
			}
			if edgeKinds != "" {
				for _, kind := range strings.Split(edgeKinds, ",") {
					req.Kind = append(req.Kind, expandEdgeKind(kind))
				}
			}
			if dotGraph {
				req.Filter = []string{"**"}
			}
			logRequest(req)
			reply, err := xs.Edges(ctx, req)
			if err != nil {
				return err
			}
			if reply.NextPageToken != "" {
				defer log.Printf("Next page token: %s", reply.NextPageToken)
			}
			if countOnly {
				return displayEdgeCounts(reply)
			} else if targetsOnly {
				return displayTargets(reply.EdgeSets)
			} else if dotGraph {
				return displayEdgeGraph(reply)
			}
			return displayEdges(reply)
		})

	cmdCallers = newCommand("callers", "[--include_overrides] <ticket>",
		"Retrieve callers of the given node",
		func(flag *flag.FlagSet) {
			flag.BoolVar(&includeOverrides, "include_overrides", false, "Whether to include overrides")
		},
		func(flag *flag.FlagSet) error {
			fmt.Fprintln(os.Stderr, "Warning: The Callers API is experimental and may be slow.")
			req := &xpb.CallersRequest{
				SemanticObject:   flag.Args(),
				IncludeOverrides: includeOverrides,
			}
			logRequest(req)
			reply, err := xs.Callers(ctx, req)
			if err != nil {
				return err
			}
			return displayCallers(reply)
		})

	cmdDocs = newCommand("docs", "<ticket>",
		"Retrieve documentation for the given node",
		func(flag *flag.FlagSet) {},
		func(flag *flag.FlagSet) error {
			fmt.Fprintf(os.Stderr, "Warning: The Documentation API is experimental and may be slow.")
			req := &xpb.DocumentationRequest{
				Ticket: flag.Args(),
			}
			logRequest(req)
			reply, err := xs.Documentation(ctx, req)
			if err != nil {
				return err
			}
			return displayDocumentation(reply)
		})

	cmdXRefs = newCommand("xrefs", "[--definitions kind] [--references kind] [--documentation kind] [--related_nodes] [--page_token token] [--page_size num] <ticket>",
		"Retrieve the global cross-references of the given node",
		func(flag *flag.FlagSet) {
			flag.StringVar(&defKind, "definitions", "all", "Kind of definitions to return (kinds: all, binding, full, or none)")
			flag.StringVar(&declKind, "declarations", "all", "Kind of declarations to return (kinds: all or none)")
			flag.StringVar(&refKind, "references", "all", "Kind of references to return (kinds: all or none)")
			flag.StringVar(&docKind, "documentation", "all", "Kind of documentation to return (kinds: all or none)")
			flag.BoolVar(&relatedNodes, "related_nodes", false, "Whether to request related nodes")

			flag.StringVar(&pageToken, "page_token", "", "CrossReferences page token")
			flag.IntVar(&pageSize, "page_size", 0, "Maximum number of cross-references returned (0 lets the service use a sensible default)")
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.CrossReferencesRequest{
				Ticket:    flag.Args(),
				PageToken: pageToken,
				PageSize:  int32(pageSize),
			}
			if relatedNodes {
				req.Filter = []string{schema.NodeKindFact, schema.SubkindFact}
			}
			switch defKind {
			case "all":
				req.DefinitionKind = xpb.CrossReferencesRequest_ALL_DEFINITIONS
			case "none":
				req.DefinitionKind = xpb.CrossReferencesRequest_NO_DEFINITIONS
			case "binding":
				req.DefinitionKind = xpb.CrossReferencesRequest_BINDING_DEFINITIONS
			case "full":
				req.DefinitionKind = xpb.CrossReferencesRequest_FULL_DEFINITIONS
			default:
				return fmt.Errorf("unknown definition kind: %q", defKind)
			}
			switch declKind {
			case "all":
				req.DeclarationKind = xpb.CrossReferencesRequest_ALL_DECLARATIONS
			case "none":
				req.DeclarationKind = xpb.CrossReferencesRequest_NO_DECLARATIONS
			default:
				return fmt.Errorf("unknown declaration kind: %q", declKind)
			}
			switch refKind {
			case "all":
				req.ReferenceKind = xpb.CrossReferencesRequest_ALL_REFERENCES
			case "none":
				req.ReferenceKind = xpb.CrossReferencesRequest_NO_REFERENCES
			default:
				return fmt.Errorf("unknown reference kind: %q", refKind)
			}
			switch docKind {
			case "all":
				req.DocumentationKind = xpb.CrossReferencesRequest_ALL_DOCUMENTATION
			case "none":
				req.DocumentationKind = xpb.CrossReferencesRequest_NO_DOCUMENTATION
			default:
				return fmt.Errorf("unknown documentation kind: %q", docKind)
			}
			logRequest(req)
			reply, err := xs.CrossReferences(ctx, req)
			if err != nil {
				return err
			}
			if reply.NextPageToken != "" {
				defer log.Printf("Next page token: %s", reply.NextPageToken)
			}
			return displayXRefs(reply)
		})

	cmdNode = newCommand("node", "[--filters factFilter1,factFilter2,...] [--max_fact_size] <ticket>",
		"Retrieve a node's facts",
		func(flag *flag.FlagSet) {
			flag.StringVar(&nodeFilters, "filters", "", "Comma-separated list of node fact filters (default returns all)")
			flag.IntVar(&factSizeThreshold, "max_fact_size", 64,
				"Maximum size of fact values to display.  Facts with byte lengths longer than this value will only have their fact names displayed.")
		},
		func(flag *flag.FlagSet) error {
			if factSizeThreshold < 0 {
				return fmt.Errorf("invalid --max_fact_size value (must be non-negative): %d", factSizeThreshold)
			}

			req := &xpb.NodesRequest{
				Ticket: flag.Args(),
			}
			if nodeFilters != "" {
				req.Filter = strings.Split(nodeFilters, ",")
			}
			logRequest(req)
			reply, err := xs.Nodes(ctx, req)
			if err != nil {
				return err
			}
			return displayNodes(reply.Nodes)
		})

	cmdSource = newCommand("source", "[--span span] <file-ticket>",
		"Retrieve a file's source text",
		func(flag *flag.FlagSet) {
			flag.StringVar(&decorSpan, "span", "", spanHelp)
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.DecorationsRequest{
				Location: &xpb.Location{
					Ticket: flag.Arg(0),
				},
				SourceText: true,
			}
			if decorSpan != "" {
				start, end, err := parseSpan(decorSpan)
				if err != nil {
					return fmt.Errorf("invalid --span %q: %v", decorSpan, err)
				}

				req.Location.Kind = xpb.Location_SPAN
				req.Location.Start = start
				req.Location.End = end
			}

			logRequest(req)
			reply, err := xs.Decorations(ctx, req)
			if err != nil {
				return err
			}
			return displaySource(reply)
		})

	cmdDecor = newCommand("decor", "[--format spec] [--dirty file] [--span span] <file-ticket>",
		"List a file's anchor decorations",
		func(flag *flag.FlagSet) {
			// TODO(schroederc): add option to look for dirty files based on file-ticket path and a directory root
			flag.StringVar(&dirtyFile, "dirty", "", "Send the given file as the dirty buffer for patching references")
			flag.StringVar(&refFormat, "format", "@edgeKind@\t@^line@:@^col@-@$line@:@$col@\t@nodeKind@\t@target@",
				`Format for each decoration result.
      Format Markers:
        @source@    -- ticket of anchor source node
        @target@    -- ticket of referenced target node
        @targetDef@ -- ticket of referenced target's definition
        @edgeKind@  -- edge kind from anchor node to its referenced target
        @nodeKind@  -- node kind of referenced target
        @subkind@   -- subkind of referenced target
        @^offset@   -- anchor source's starting byte-offset
        @^line@     -- anchor source's starting line
        @^col@      -- anchor source's starting column offset
        @$offset@   -- anchor source's ending byte-offset
        @$line@     -- anchor source's ending line
        @$col@      -- anchor source's ending column offset`)
			flag.StringVar(&decorSpan, "span", "", spanHelp)
			flag.BoolVar(&targetDefs, "target_definitions", false, "Whether to request definitions (@targetDef@ format marker) for each reference's target")
		},
		func(flag *flag.FlagSet) error {
			req := &xpb.DecorationsRequest{
				Location: &xpb.Location{
					Ticket: flag.Arg(0),
				},
				References:        true,
				TargetDefinitions: targetDefs,
				Filter: []string{
					schema.NodeKindFact,
					schema.SubkindFact,
				},
			}
			if dirtyFile != "" {
				f, err := vfs.Open(ctx, dirtyFile)
				if err != nil {
					return fmt.Errorf("error opening dirty buffer file at %q: %v", dirtyFile, err)
				}
				buf, err := ioutil.ReadAll(f)
				if err != nil {
					f.Close()
					return fmt.Errorf("error reading dirty buffer file: %v", err)
				} else if err := f.Close(); err != nil {
					return fmt.Errorf("error closing dirty buffer file: %v", err)
				}
				req.DirtyBuffer = buf
			}
			if decorSpan != "" {
				start, end, err := parseSpan(decorSpan)
				if err != nil {
					return fmt.Errorf("invalid --span %q: %v", decorSpan, err)
				}

				req.Location.Kind = xpb.Location_SPAN
				req.Location.Start = start
				req.Location.End = end
			}

			logRequest(req)
			reply, err := xs.Decorations(ctx, req)
			if err != nil {
				return err
			}

			return displayDecorations(reply)
		})
)