kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/test/services/xrefs/xrefs.go (about)

     1  /*
     2   * Copyright 2017 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  // Package xrefs provides test utilities for the xrefs.Service.
    18  //
    19  // This package includes the Atomizer which allows for XRefService proto
    20  // response messages to be shattered in GraphStore entries.  By indexing,
    21  // post-processing, and serving source code with verifier
    22  // (http://www.kythe.io/docs/kythe-verifier.html) goals, using the Atomizer to
    23  // shatter the served DecorationsReply and CrossReferencesReply messages, and
    24  // feeding the resulting entries into the verifier, one can write integration
    25  // tests between the Kythe indexers and Kythe server.
    26  package xrefs // import "kythe.io/kythe/go/test/services/xrefs"
    27  
    28  import (
    29  	"context"
    30  	"fmt"
    31  	"path/filepath"
    32  	"strings"
    33  
    34  	"kythe.io/kythe/go/util/kytheuri"
    35  	"kythe.io/kythe/go/util/log"
    36  	"kythe.io/kythe/go/util/markedsource"
    37  	"kythe.io/kythe/go/util/schema/edges"
    38  	"kythe.io/kythe/go/util/schema/facts"
    39  	"kythe.io/kythe/go/util/schema/nodes"
    40  
    41  	"google.golang.org/protobuf/proto"
    42  
    43  	cpb "kythe.io/kythe/proto/common_go_proto"
    44  	spb "kythe.io/kythe/proto/storage_go_proto"
    45  	xpb "kythe.io/kythe/proto/xref_go_proto"
    46  )
    47  
    48  // An Atomizer shatters XRefService proto replies into equivalent GraphStore
    49  // entries.  The resulting composition of the emitted entries reflects a Kythe
    50  // graph following the Kythe schema in most respects.  In particular, the
    51  // emitted graph approximates an input Kythe graph that was used to derive the
    52  // XRefService replies (through post-processing and serving).
    53  type Atomizer func(context.Context, *spb.Entry) error
    54  
    55  type atomizerPanic struct{ error }
    56  
    57  // catchErrors recovers from a possible panic.  If the panic was caused by a
    58  // atomizerPanic, the underlying error will replace the ret error.
    59  // Otherwise, the panic error will directly replace the ret error.  If
    60  // recovering from a panic and *ret != nil, the original ret error will be
    61  // logged as a warning.
    62  func (a Atomizer) catchErrors(ret *error) {
    63  	if err := recover(); err != nil {
    64  		if pe, ok := err.(atomizerPanic); ok {
    65  			if *ret != nil {
    66  				log.Warningf("%v", *ret)
    67  			}
    68  			*ret = pe.error
    69  		} else {
    70  			panic(err)
    71  		}
    72  	}
    73  }
    74  
    75  func (a Atomizer) emit(ctx context.Context, entry *spb.Entry) {
    76  	if err := a(ctx, entry); err != nil {
    77  		// Instead of checking for errors throughout the code, panic with a special
    78  		// error that will be caught in the top-level methods by
    79  		// Atomizer.catchErrors.
    80  		panic(atomizerPanic{err})
    81  	}
    82  }
    83  
    84  // Decorations emits a file node for the reply's Location, emits an anchor node
    85  // for each reference along with an edge to its target, and emits all node
    86  // facts directly.
    87  func (a Atomizer) Decorations(ctx context.Context, decor *xpb.DecorationsReply) (ret error) {
    88  	defer a.catchErrors(&ret)
    89  	file := a.parseTicket(decor.Location.Ticket)
    90  	a.emitFact(ctx, file, facts.NodeKind, []byte(nodes.File))
    91  	a.emitFact(ctx, file, facts.Text, decor.SourceText)
    92  	a.emitFact(ctx, file, facts.TextEncoding, []byte(decor.Encoding))
    93  	for _, ref := range decor.Reference {
    94  		anchor := a.anchorNode(ctx, decor.Location.Ticket, ref.Span, nil)
    95  		a.emitEdge(ctx, anchor, ref.Kind, a.parseTicket(ref.TargetTicket))
    96  	}
    97  	for ticket, node := range decor.Nodes {
    98  		for name, val := range node.Facts {
    99  			a.emitFact(ctx, a.parseTicket(ticket), name, val)
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  // CrossReferences emits anchor nodes for all RelatedAnchors along with edges to
   106  // their targets, emits edges for each RelatedNode, and emits all node facts
   107  // directly.
   108  // TODO(schroederc): handle Callers
   109  func (a Atomizer) CrossReferences(ctx context.Context, xrefs *xpb.CrossReferencesReply) (ret error) {
   110  	defer a.catchErrors(&ret)
   111  	for _, xrs := range xrefs.CrossReferences {
   112  		src := a.parseTicket(xrs.Ticket)
   113  		if xrs.MarkedSource != nil {
   114  			rec, err := proto.Marshal(xrs.MarkedSource)
   115  			if err != nil {
   116  				return err
   117  			}
   118  			a.emitFact(ctx, src, facts.Code, rec)
   119  
   120  			rendered := markedsource.Render(xrs.MarkedSource)
   121  			a.emitFact(ctx, src, facts.Code+"/rendered", []byte(rendered))
   122  			ident := markedsource.RenderSimpleIdentifier(xrs.MarkedSource, markedsource.PlaintextContent, nil)
   123  			a.emitFact(ctx, src, facts.Code+"/rendered/identifier", []byte(ident))
   124  			params := markedsource.RenderSimpleParams(xrs.MarkedSource, markedsource.PlaintextContent, nil)
   125  			if len(params) > 0 {
   126  				a.emitFact(ctx, src, facts.Code+"/rendered/params", []byte(strings.Join(params, ",")))
   127  			}
   128  		}
   129  		a.emitAnchors(ctx, src, xrs.Definition)
   130  		a.emitAnchors(ctx, src, xrs.Declaration)
   131  		a.emitAnchors(ctx, src, xrs.Reference)
   132  		a.emitAnchors(ctx, src, xrs.Caller)
   133  		for _, rn := range xrs.RelatedNode {
   134  			if rn.RelationKind == edges.Param || rn.Ordinal != 0 {
   135  				a.emitOrdinal(ctx, src, rn.RelationKind, a.parseTicket(rn.Ticket), rn.Ordinal)
   136  			} else {
   137  				a.emitEdge(ctx, src, rn.RelationKind, a.parseTicket(rn.Ticket))
   138  			}
   139  		}
   140  	}
   141  	for ticket, node := range xrefs.Nodes {
   142  		for name, val := range node.Facts {
   143  			a.emitFact(ctx, a.parseTicket(ticket), name, val)
   144  		}
   145  	}
   146  	return nil
   147  }
   148  
   149  func (a Atomizer) parseTicket(ticket string) *spb.VName {
   150  	uri, err := kytheuri.Parse(ticket)
   151  	if err != nil {
   152  		panic(atomizerPanic{err})
   153  	}
   154  	return uri.VName()
   155  }
   156  
   157  // anchorURI synthesizes an anchor URI from a span and its parent file.
   158  func (a Atomizer) anchorURI(fileTicket string, span *cpb.Span) *kytheuri.URI {
   159  	uri, err := kytheuri.Parse(fileTicket)
   160  	if err != nil {
   161  		panic(atomizerPanic{err})
   162  	}
   163  	uri.Signature = fmt.Sprintf("a[%d,%d)", span.GetStart().GetByteOffset(), span.GetEnd().GetByteOffset())
   164  	// The language doesn't have to exactly match the schema; just use the file's
   165  	// extension as an approximation.
   166  	uri.Language = strings.TrimPrefix(filepath.Ext(uri.Path), ".")
   167  	return uri
   168  }
   169  
   170  func (a Atomizer) anchorNode(ctx context.Context, parentFile string, span *cpb.Span, snippet *cpb.Span) *spb.VName {
   171  	anchor := a.anchorURI(parentFile, span).VName()
   172  	a.emitFact(ctx, anchor, facts.NodeKind, []byte(nodes.Anchor))
   173  	a.emitFact(ctx, anchor, facts.AnchorStart, []byte(fmt.Sprintf("%d", span.Start.ByteOffset)))
   174  	a.emitFact(ctx, anchor, facts.AnchorEnd, []byte(fmt.Sprintf("%d", span.End.ByteOffset)))
   175  	if snippet != nil {
   176  		a.emitFact(ctx, anchor, facts.SnippetStart, []byte(fmt.Sprintf("%d", snippet.Start.ByteOffset)))
   177  		a.emitFact(ctx, anchor, facts.SnippetEnd, []byte(fmt.Sprintf("%d", snippet.End.ByteOffset)))
   178  	}
   179  	return anchor
   180  }
   181  
   182  func (a Atomizer) emitFact(ctx context.Context, src *spb.VName, fact string, value []byte) {
   183  	a.emit(ctx, &spb.Entry{
   184  		Source:    src,
   185  		FactName:  fact,
   186  		FactValue: value,
   187  	})
   188  }
   189  
   190  func (a Atomizer) emitOrdinal(ctx context.Context, src *spb.VName, kind string, tgt *spb.VName, ordinal int32) {
   191  	a.emitEdge(ctx, src, fmt.Sprintf("%s.%d", kind, ordinal), tgt)
   192  }
   193  
   194  func (a Atomizer) emitEdge(ctx context.Context, src *spb.VName, kind string, tgt *spb.VName) {
   195  	a.emit(ctx, &spb.Entry{
   196  		Source:   src,
   197  		FactName: "/",
   198  		EdgeKind: kind,
   199  		Target:   tgt,
   200  	})
   201  }
   202  
   203  func (a Atomizer) emitAnchors(ctx context.Context, n *spb.VName, ras []*xpb.CrossReferencesReply_RelatedAnchor) {
   204  	for _, ra := range ras {
   205  		anchor := a.anchorNode(ctx, ra.Anchor.Parent, ra.Anchor.Span, ra.Anchor.SnippetSpan)
   206  		a.emitEdge(ctx, anchor, ra.Anchor.Kind, n)
   207  	}
   208  }