All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sourcegraph.semanticdb_javac.SemanticdbVisitor Maven / Gradle / Ivy

There is a newer version: 0.10.3
Show newest version
package com.sourcegraph.semanticdb_javac;

import com.sun.source.tree.*;
import com.sun.source.util.*;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.model.JavacTypes;
import com.sun.tools.javac.tree.EndPosTable;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.JCDiagnostic;
import com.sun.tools.javac.util.Position;
import com.sourcegraph.semanticdb_javac.Semanticdb.SymbolInformation.Kind;
import com.sourcegraph.semanticdb_javac.Semanticdb.SymbolInformation.Property;
import com.sourcegraph.semanticdb_javac.Semanticdb.SymbolOccurrence.Role;

import javax.lang.model.element.ElementKind;
import javax.lang.model.util.Elements;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.stream.Collectors;

import static com.sourcegraph.semanticdb_javac.SemanticdbBuilders.*;
import static com.sourcegraph.semanticdb_javac.SemanticdbTypeVisitor.ARRAY_SYMBOL;

/** Walks the AST of a typechecked compilation unit and generates a SemanticDB TextDocument. */
public class SemanticdbVisitor extends TreePathScanner {

  private final GlobalSymbolsCache globals;
  private final LocalSymbolsCache locals;
  private final JavacTask task;
  private final TaskEvent event;
  private final JavacTypes javacTypes;
  private final Trees trees;
  private final SemanticdbJavacOptions options;
  private final EndPosTable endPosTable;
  private final ArrayList occurrences;
  private final ArrayList symbolInfos;
  private String source;

  public SemanticdbVisitor(
      JavacTask task,
      GlobalSymbolsCache globals,
      TaskEvent event,
      SemanticdbJavacOptions options,
      JavacTypes javacTypes) {
    this.task = task;
    this.globals = globals; // Reused cache between compilation units.
    this.locals = new LocalSymbolsCache(); // Fresh cache per compilation unit.
    this.event = event;
    this.options = options;
    this.javacTypes = javacTypes;
    this.trees = Trees.instance(task);
    if (event.getCompilationUnit() instanceof JCTree.JCCompilationUnit) {
      this.endPosTable = ((JCTree.JCCompilationUnit) event.getCompilationUnit()).endPositions;
    } else {
      this.endPosTable = new EmptyEndPosTable();
    }
    this.occurrences = new ArrayList<>();
    this.symbolInfos = new ArrayList<>();
    this.source = semanticdbText();
  }

  public Semanticdb.TextDocument buildTextDocument(CompilationUnitTree tree) {
    this.scan(tree, null); // Trigger recursive AST traversal to collect SemanticDB information.

    return Semanticdb.TextDocument.newBuilder()
        .setSchema(Semanticdb.Schema.SEMANTICDB4)
        .setLanguage(Semanticdb.Language.JAVA)
        .setUri(semanticdbUri())
        .setText(options.includeText ? this.source : "")
        .setMd5(semanticdbMd5())
        .addAllOccurrences(occurrences)
        .addAllSymbols(symbolInfos)
        .build();
  }

  private  void emitSymbolOccurrence(
      Symbol sym, T posTree, Role role, CompilerRange kind) {
    if (sym == null) return;
    Optional occ = semanticdbOccurrence(sym, posTree, kind, role);
    occ.ifPresent(occurrences::add);
    if (role == Role.DEFINITION) {
      // Only emit SymbolInformation for symbols that are defined in this compilation unit.
      emitSymbolInformation(sym, posTree);
    }
  }

  private void emitSymbolInformation(Symbol sym, JCTree tree) {
    Semanticdb.SymbolInformation.Builder builder = symbolInformation(semanticdbSymbol(sym));
    Semanticdb.Documentation documentation = semanticdbDocumentation(sym);
    if (documentation != null) builder.setDocumentation(documentation);
    Semanticdb.Signature signature = semanticdbSignature(sym);
    if (signature != null) builder.setSignature(signature);
    List annotations =
        new SemanticdbTrees(globals, locals, semanticdbUri()).annotations(tree);
    if (annotations != null) builder.addAllAnnotations(annotations);

    builder
        .setProperties(semanticdbSymbolInfoProperties(sym))
        .setDisplayName(sym.name.toString())
        .setAccess(semanticdbAccess(sym));

    switch (sym.getKind()) {
      case ENUM:
      case CLASS:
        builder.setKind(Kind.CLASS);
        builder.addAllOverriddenSymbols(semanticdbParentSymbols(sym, new ArrayList<>()));
        break;
      case INTERFACE:
      case ANNOTATION_TYPE:
        builder.setKind(Kind.INTERFACE);
        builder.addAllOverriddenSymbols(semanticdbParentSymbols(sym, new ArrayList<>()));
        break;
      case FIELD:
        builder.setKind(Kind.FIELD);
        break;
      case METHOD:
        builder.setKind(Kind.METHOD);
        builder.addAllOverriddenSymbols(semanticdbOverrides(sym));
        break;
      case CONSTRUCTOR:
        builder.setKind(Kind.CONSTRUCTOR);
        break;
      case TYPE_PARAMETER:
        builder.setKind(Kind.TYPE_PARAMETER);
        break;
      case ENUM_CONSTANT: // overwrite previous value here
        String args =
            ((JCTree.JCNewClass) ((JCTree.JCVariableDecl) tree).init)
                .args.stream().map(JCTree::toString).collect(Collectors.joining(", "));
        if (!args.isEmpty()) builder.setDisplayName(sym.name.toString() + "(" + args + ")");
    }

    Semanticdb.SymbolInformation info = builder.build();

    symbolInfos.add(info);
  }

  // =======================================
  // Overridden methods from TreePathScanner
  // =======================================

  @Override
  public Void visitClass(ClassTree node, Void unused) {
    if (node instanceof JCTree.JCClassDecl) {
      JCTree.JCClassDecl cls = (JCTree.JCClassDecl) node;
      if (cls.sym == null) return super.visitClass(node, unused);
      emitSymbolOccurrence(
          cls.sym, cls, Role.DEFINITION, CompilerRange.FROM_POINT_WITH_TEXT_SEARCH);

      List typeParameters = cls.getTypeParameters();
      int i = 0;
      for (Symbol.TypeVariableSymbol typeSym : cls.sym.getTypeParameters()) {
        if (i >= typeParameters.size()) {
          // Happens in testcontainers/testcontainers-java, see
          // https://github.com/sourcegraph/lsif-java/issues/319
          // Failed to reproduce with a minimal source file so we don't have a test case that hits
          // this branch.
          break;
        }
        emitSymbolOccurrence(
            typeSym,
            typeParameters.get(i),
            Role.DEFINITION,
            CompilerRange.FROM_POINT_TO_SYMBOL_NAME);
        i++;
      }
    }
    return super.visitClass(node, unused);
  }

  @Override
  public Void visitMethod(MethodTree node, Void unused) {
    if (node instanceof JCTree.JCMethodDecl) {
      JCTree.JCMethodDecl meth = (JCTree.JCMethodDecl) node;
      if (meth.sym == null) return super.visitMethod(node, unused);
      CompilerRange range = CompilerRange.FROM_POINT_TO_SYMBOL_NAME;
      if (meth.sym.isConstructor()) {
        if (meth.sym.owner.isAnonymous()) return null;
        range = CompilerRange.FROM_POINT_WITH_TEXT_SEARCH;
      }
      emitSymbolOccurrence(meth.sym, meth, Role.DEFINITION, range);

      List typeParameters = meth.getTypeParameters();
      int i = 0;
      for (Symbol.TypeVariableSymbol typeSym : meth.sym.getTypeParameters()) {
        emitSymbolOccurrence(
            typeSym,
            typeParameters.get(i),
            Role.DEFINITION,
            CompilerRange.FROM_POINT_TO_SYMBOL_NAME);
        i++;
      }
    }
    return super.visitMethod(node, unused);
  }

  @Override
  public Void visitVariable(VariableTree node, Void unused) {
    if (node instanceof JCTree.JCVariableDecl) {
      JCTree.JCVariableDecl decl = (JCTree.JCVariableDecl) node;
      emitSymbolOccurrence(
          decl.sym, decl, Role.DEFINITION, CompilerRange.FROM_POINT_TO_SYMBOL_NAME);
    }
    return super.visitVariable(node, unused);
  }

  @Override
  public Void visitIdentifier(IdentifierTree node, Void unused) {
    if (node instanceof JCTree.JCIdent) {
      JCTree.JCIdent ident = (JCTree.JCIdent) node;
      if (ident.name == null || ident.sym == null) return null;
      if (ident.name.toString().equals("this") && ident.sym.getKind() != ElementKind.CONSTRUCTOR)
        return null;
      emitSymbolOccurrence(ident.sym, ident, Role.REFERENCE, CompilerRange.FROM_START_TO_END);
    }
    return super.visitIdentifier(node, unused);
  }

  @Override
  public Void visitMemberReference(MemberReferenceTree node, Void unused) {
    if (node instanceof JCTree.JCMemberReference) {
      JCTree.JCMemberReference ref = (JCTree.JCMemberReference) node;
      emitSymbolOccurrence(ref.sym, ref, Role.REFERENCE, CompilerRange.FROM_END_WITH_TEXT_SEARCH);
    }
    return super.visitMemberReference(node, unused);
  }

  @Override
  public Void visitMemberSelect(MemberSelectTree node, Void unused) {
    if (node instanceof JCTree.JCFieldAccess) {
      JCTree.JCFieldAccess select = (JCTree.JCFieldAccess) node;
      emitSymbolOccurrence(
          select.sym, select, Role.REFERENCE, CompilerRange.FROM_POINT_TO_SYMBOL_NAME_PLUS_ONE);
    }
    return super.visitMemberSelect(node, unused);
  }

  @Override
  public Void visitNewClass(NewClassTree node, Void unused) {
    if (node instanceof JCTree.JCNewClass) {
      JCTree.JCNewClass cls = (JCTree.JCNewClass) node;
      if (cls.type != null && cls.type.tsym != null && !cls.type.tsym.isAnonymous()) {
        emitSymbolOccurrence(cls.constructor, cls, Role.REFERENCE, CompilerRange.FROM_TEXT_SEARCH);
      }
    }

    // to avoid emitting a reference to the class itself, we manually scan everything
    // except the identifier
    scan(node.getTypeArguments(), unused);
    scan(node.getArguments(), unused);
    return scan(node.getClassBody(), unused);
  }

  // =================================================
  // Utilities to generate SemanticDB data structures.
  // =================================================

  private Semanticdb.Signature semanticdbSignature(Symbol sym) {

    return new SemanticdbSignatures(globals, locals).generateSignature(sym);
  }

  private String semanticdbSymbol(Symbol sym) {
    return globals.semanticdbSymbol(sym, locals);
  }

  private Optional semanticdbRange(
      JCDiagnostic.DiagnosticPosition pos, CompilerRange kind, Symbol sym) {
    LineMap lineMap = event.getCompilationUnit().getLineMap();
    if (sym == null) return Optional.empty();
    int start, end;
    if (kind.isFromPoint() && sym.name != null) {
      start = pos.getPreferredPosition();
      if (kind == CompilerRange.FROM_POINT_TO_SYMBOL_NAME_PLUS_ONE) {
        start++;
      }
      end = start + sym.name.length();
    } else {
      start = pos.getStartPosition();
      end = pos.getEndPosition(endPosTable);
    }

    if (kind.isFromTextSearch() && sym.name.length() > 0) {
      Optional range =
          RangeFinder.findRange(
              getCurrentPath(),
              trees,
              getCurrentPath().getCompilationUnit(),
              sym,
              start,
              this.source,
              kind.isFromEnd());
      if (range.isPresent()) return Optional.of(correctForTabs(range.get(), lineMap, start));
      else return range;
    } else if (start != Position.NOPOS && end != Position.NOPOS && end > start) {
      Semanticdb.Range range =
          Semanticdb.Range.newBuilder()
              .setStartLine((int) lineMap.getLineNumber(start) - 1)
              .setStartCharacter((int) lineMap.getColumnNumber(start) - 1)
              .setEndLine((int) lineMap.getLineNumber(end) - 1)
              .setEndCharacter((int) lineMap.getColumnNumber(end) - 1)
              .build();

      range = correctForTabs(range, lineMap, start);

      return Optional.of(range);
    }

    return Optional.empty();
  }

  private Semanticdb.Range correctForTabs(Semanticdb.Range range, LineMap lineMap, int start) {
    int startLinePos = (int) lineMap.getPosition(lineMap.getLineNumber(start), 0);

    // javac replaces every tab with 8 spaces in the linemap. As this is potentially inconsistent
    // with the source file itself, we adjust for that here if the line is actually indented with
    // tabs.
    // As for every tab there are 8 spaces, we remove 7 spaces for every tab to get the correct
    // char offset (note: different to _column_ offset your editor shows)
    if (this.source.charAt(startLinePos) == '\t') {
      int count = 1;
      while (this.source.charAt(++startLinePos) == '\t') count++;
      range =
          range
              .toBuilder()
              .setStartCharacter(range.getStartCharacter() - (count * 7))
              .setEndCharacter(range.getEndCharacter() - (count * 7))
              .build();
    }

    return range;
  }

  private Optional semanticdbOccurrence(
      Symbol sym, JCDiagnostic.DiagnosticPosition pos, CompilerRange kind, Role role) {
    Optional range = semanticdbRange(pos, kind, sym);
    if (range.isPresent()) {
      String ssym = semanticdbSymbol(sym);
      if (!ssym.equals(SemanticdbSymbols.NONE)) {
        Semanticdb.SymbolOccurrence occ = symbolOccurrence(ssym, range.get(), role);
        return Optional.of(occ);
      } else {
        return Optional.empty();
      }
    } else {
      return Optional.empty();
    }
  }

  private String semanticdbText() {
    if (source != null) return source;
    try {
      source = event.getSourceFile().getCharContent(true).toString();
    } catch (IOException e) {
      source = "";
    }
    return source;
  }

  private String semanticdbMd5() {
    try {
      return MD5.digest(event.getSourceFile().getCharContent(true).toString());
    } catch (IOException | NoSuchAlgorithmException e) {
      return "";
    }
  }

  private int semanticdbSymbolInfoProperties(Symbol sym) {
    int properties = 0;
    properties |= sym.isEnum() ? Property.ENUM_VALUE : 0;
    properties |= sym.isStatic() ? Property.STATIC_VALUE : 0;
    // for default interface methods, Flags.ABSTRACT is also set...
    boolean abstractNotDefault =
        ((sym.flags() & Flags.ABSTRACT) > 0) && ((sym.flags() & Flags.DEFAULT) == 0);
    properties |= abstractNotDefault ? Property.ABSTRACT_VALUE : 0;
    properties |= (sym.flags() & Flags.FINAL) > 0 ? Property.FINAL_VALUE : 0;
    properties |= (sym.flags() & Flags.DEFAULT) > 0 ? Property.DEFAULT_VALUE : 0;
    return properties;
  }

  private List semanticdbParentSymbols(Symbol sym, List result) {
    if (!(sym instanceof Symbol.ClassSymbol)) {
      return result;
    }
    Symbol.ClassSymbol csym = (Symbol.ClassSymbol) sym;
    if (csym.getSuperclass() != Type.noType) {
      semanticdbParentSymbol(csym.getSuperclass().tsym, result);
    }
    for (Type iType : csym.getInterfaces()) {
      semanticdbParentSymbol(iType.tsym, result);
    }
    return result;
  }

  private void semanticdbParentSymbol(Symbol sym, List result) {
    if (sym == null) {
      return;
    }
    String ssym = semanticdbSymbol(sym);
    if (!Objects.equals(ssym, SemanticdbSymbols.NONE)) {
      result.add(ssym);
      semanticdbParentSymbols(sym, result);
    }
  }

  private List semanticdbOverrides(Symbol sym) {
    ArrayList overriddenSymbols = new ArrayList<>();
    Set overriddenMethods = javacTypes.getOverriddenMethods(sym);

    for (Symbol.MethodSymbol meth : overriddenMethods) {
      overriddenSymbols.add(semanticdbSymbol(meth));
    }

    return overriddenSymbols;
  }

  private Semanticdb.Access semanticdbAccess(Symbol sym) {
    switch ((int) sym.flags() & Flags.AccessFlags) {
      case Flags.PRIVATE:
        return privateAccess();
      case Flags.PUBLIC:
        return publicAccess();
      case Flags.PROTECTED:
        return protectedAccess();
      case 0:
        return privateWithinAccess(semanticdbSymbol(sym.owner));
      default:
        throw new IllegalStateException("access flag " + (sym.flags() & Flags.AccessFlags));
    }
  }

  private String semanticdbUri() {
    Path absolutePath = SemanticdbTaskListener.absolutePathFromUri(options, event.getSourceFile());
    Path relativePath = options.sourceroot.relativize(absolutePath);
    StringBuilder out = new StringBuilder();
    Iterator it = relativePath.iterator();
    if (it.hasNext()) out.append(it.next().getFileName().toString());
    while (it.hasNext()) {
      Path part = it.next();
      out.append('/').append(part.getFileName().toString());
    }
    return out.toString();
  }

  private Semanticdb.Documentation semanticdbDocumentation(Symbol sym) {
    try {
      Elements elements = task.getElements();
      if (elements == null) return null;

      String doc = elements.getDocComment(sym);
      if (doc == null) return null;

      return Semanticdb.Documentation.newBuilder()
          .setFormat(Semanticdb.Documentation.Format.JAVADOC)
          .setMessage(doc)
          .build();
    } catch (NullPointerException e) {
      // Can happen in `getDocComment()`
      // Caused by: java.lang.NullPointerException
      //   at com.sun.tools.javac.model.JavacElements.cast(JavacElements.java:605)
      //   at com.sun.tools.javac.model.JavacElements.getTreeAndTopLevel(JavacElements.java:543)
      //   at com.sun.tools.javac.model.JavacElements.getDocComment(JavacElements.java:321)
      //   at
      // com.sourcegraph.semanticdb_javac.SemanticdbVisitor.semanticdbDocumentation(SemanticdbVisitor.java:233)
      return null;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy