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

org.sonar.go.plugin.converter.ASTConverterValidation Maven / Gradle / Ivy

There is a newer version: 1.1.1.2000
Show newest version
/*
 * SonarSource Go
 * Copyright (C) 2018-2025 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.go.plugin.converter;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.config.Configuration;
import org.sonar.go.api.ASTConverter;
import org.sonar.go.api.Comment;
import org.sonar.go.api.IdentifierTree;
import org.sonar.go.api.TextPointer;
import org.sonar.go.api.TextRange;
import org.sonar.go.api.Token;
import org.sonar.go.api.TopLevelTree;
import org.sonar.go.api.Tree;
import org.sonar.go.api.TreeMetaData;
import org.sonar.go.impl.LiteralTreeImpl;
import org.sonar.go.impl.NativeTreeImpl;
import org.sonar.go.impl.PlaceHolderTreeImpl;
import org.sonar.go.impl.TextPointerImpl;

import static org.sonar.go.utils.LogArg.lazyArg;

public class ASTConverterValidation implements ASTConverter {

  private static final Logger LOG = LoggerFactory.getLogger(ASTConverterValidation.class);

  private static final Pattern PUNCTUATOR_PATTERN = Pattern.compile("[^0-9A-Za-z]++");

  private static final Set ALLOWED_MISPLACED_TOKENS_OUTSIDE_PARENT_RANGE = new HashSet<>(Collections.singleton("implicit"));

  private final ASTConverter wrapped;

  private final Map firstErrorOfEachKind = new TreeMap<>();

  private final ValidationMode mode;

  public enum ValidationMode {
    THROW_EXCEPTION,
    LOG_ERROR
  }

  @Nullable
  private String currentFile = null;

  public ASTConverterValidation(ASTConverter wrapped, ValidationMode mode) {
    this.wrapped = wrapped;
    this.mode = mode;
  }

  public static ASTConverter wrap(ASTConverter converter, Configuration configuration) {
    String mode = configuration.get("sonar.slang.converter.validation").orElse(null);
    if (mode == null) {
      return converter;
    } else if (mode.equals("throw")) {
      return new ASTConverterValidation(converter, ValidationMode.THROW_EXCEPTION);
    } else if (mode.equals("log")) {
      return new ASTConverterValidation(converter, ValidationMode.LOG_ERROR);
    } else {
      throw new IllegalStateException("Unsupported mode: " + mode);
    }
  }

  @Override
  public Tree parse(String content) {
    return parse(content, null);
  }

  @Override
  public Tree parse(String content, @Nullable String currentFile) {
    this.currentFile = currentFile;
    Tree tree = wrapped.parse(content, currentFile);
    assertTreeIsValid(tree);
    assertTokensMatchSourceCode(tree, content);
    return tree;
  }

  @Override
  public void terminate() {
    List errors = errors();
    if (!errors.isEmpty()) {
      String delimiter = "\n  [AST ERROR] ";
      LOG.error("AST Converter Validation detected {} errors:{}{}", errors.size(), delimiter, lazyArg(() -> String.join(delimiter, errors)));
    }
    wrapped.terminate();
  }

  ValidationMode mode() {
    return mode;
  }

  List errors() {
    return firstErrorOfEachKind.entrySet().stream()
      .map(entry -> entry.getKey() + entry.getValue())
      .toList();
  }

  private void raiseError(String messageKey, String messageDetails, TextPointer position) {
    if (mode == ValidationMode.THROW_EXCEPTION) {
      throw new IllegalStateException("ASTConverterValidationException: " + messageKey + messageDetails +
        " at  " + position.line() + ":" + position.lineOffset());
    } else {
      String positionDetails = String.format(" (line: %d, column: %d)", position.line(), (position.lineOffset() + 1));
      if (currentFile != null) {
        positionDetails += " in file: " + currentFile;
      }
      firstErrorOfEachKind.putIfAbsent(messageKey, messageDetails + positionDetails);
    }
  }

  private static String kind(Tree tree) {
    return tree.getClass().getSimpleName();
  }

  private void assertTreeIsValid(Tree tree) {
    assertTextRangeIsValid(tree);
    assertTreeHasAtLeastOneToken(tree);
    assertTokensAndChildTokens(tree);
    for (Tree child : tree.children()) {
      if (child == null) {
        raiseError(kind(tree) + " has a null child", "", tree.textRange().start());
      } else if (child.metaData() == null) {
        raiseError(kind(child) + " metaData is null", "", tree.textRange().start());
      } else {
        assertTreeIsValid(child);
      }
    }
  }

  private void assertTextRangeIsValid(Tree tree) {
    TextPointer start = tree.metaData().textRange().start();
    TextPointer end = tree.metaData().textRange().end();

    boolean startOffsetAfterEndOffset = !(tree instanceof TopLevelTree) &&
      start.line() == end.line() &&
      start.lineOffset() >= end.lineOffset();

    if (start.line() <= 0 || end.line() <= 0 ||
      start.line() > end.line() ||
      start.lineOffset() < 0 || end.lineOffset() < 0 ||
      startOffsetAfterEndOffset) {
      raiseError(kind(tree) + " invalid range ", tree.metaData().textRange().toString(), start);
    }
  }

  private void assertTreeHasAtLeastOneToken(Tree tree) {
    if (!(tree instanceof TopLevelTree) && tree.metaData().tokens().isEmpty()) {
      raiseError(kind(tree) + " has no token", "", tree.textRange().start());
    }
  }

  private void assertTokensMatchSourceCode(Tree tree, String code) {
    CodeFormToken codeFormToken = new CodeFormToken(tree.metaData());
    codeFormToken.assertEqualTo(code);
  }

  private void assertTokensAndChildTokens(Tree tree) {
    assertTokensAreInsideRange(tree);
    Set parentTokens = new HashSet<>(tree.metaData().tokens());
    Map childByToken = new HashMap<>();
    for (Tree child : tree.children()) {
      if (child != null && child.metaData() != null && !isAllowedMisplacedTree(child)) {
        assertChildRangeIsInsideParentRange(tree, child);
        assertChildTokens(parentTokens, childByToken, tree, child);
      }
    }
    parentTokens.removeAll(childByToken.keySet());
    assertUnexpectedTokenKind(tree, parentTokens);
  }

  private static boolean isAllowedMisplacedTree(Tree tree) {
    List tokens = tree.metaData().tokens();
    return tokens.size() == 1 && ALLOWED_MISPLACED_TOKENS_OUTSIDE_PARENT_RANGE.contains(tokens.get(0).text());
  }

  private void assertUnexpectedTokenKind(Tree tree, Set tokens) {
    if (tree instanceof NativeTreeImpl || tree instanceof LiteralTreeImpl || tree instanceof PlaceHolderTreeImpl) {
      return;
    }
    List unexpectedTokens;
    if (tree instanceof IdentifierTree) {
      unexpectedTokens = tokens.stream()
        .filter(token -> token.type() == Token.Type.KEYWORD || token.type() == Token.Type.STRING_LITERAL)
        .toList();
    } else {
      unexpectedTokens = tokens.stream()
        .filter(token -> token.type() != Token.Type.KEYWORD)
        .filter(token -> !PUNCTUATOR_PATTERN.matcher(token.text()).matches())
        .filter(token -> !ALLOWED_MISPLACED_TOKENS_OUTSIDE_PARENT_RANGE.contains(token.text()))
        .toList();
    }
    if (!unexpectedTokens.isEmpty()) {
      String tokenList = unexpectedTokens.stream()
        .sorted(Comparator.comparing(token -> token.textRange().start()))
        .map(Token::text)
        .collect(Collectors.joining("', '"));
      raiseError("Unexpected tokens in " + kind(tree), ": '" + tokenList + "'", tree.textRange().start());
    }
  }

  private void assertTokensAreInsideRange(Tree tree) {
    TextRange parentRange = tree.metaData().textRange();
    tree.metaData().tokens().stream()
      .filter(token -> !ALLOWED_MISPLACED_TOKENS_OUTSIDE_PARENT_RANGE.contains(token.text()))
      .filter(token -> !token.textRange().isInside(parentRange))
      .findFirst()
      .ifPresent(token -> raiseError(
        kind(tree) + " contains a token outside its range",
        " range: " + parentRange + " tokenRange: " + token.textRange() + " token: '" + token.text() + "'",
        token.textRange().start()));
  }

  private void assertChildRangeIsInsideParentRange(Tree parent, Tree child) {
    TextRange parentRange = parent.metaData().textRange();
    TextRange childRange = child.metaData().textRange();
    if (!childRange.isInside(parentRange)) {
      raiseError(kind(parent) + " contains a child " + kind(child) + " outside its range",
        ", parentRange: " + parentRange + " childRange: " + childRange,
        childRange.start());
    }
  }

  private void assertChildTokens(Set parentTokens, Map childByToken, Tree parent, Tree child) {
    for (Token token : child.metaData().tokens()) {
      if (!parentTokens.contains(token)) {
        raiseError(kind(child) + " contains a token missing in its parent " + kind(parent),
          ", token: '" + token.text() + "'",
          token.textRange().start());
      }
      Tree intersectingChild = childByToken.get(token);
      if (intersectingChild != null) {
        raiseError(kind(parent) + " has a token used by both children " + kind(intersectingChild) + " and " + kind(child),
          ", token: '" + token.text() + "'",
          token.textRange().start());
      } else {
        childByToken.put(token, child);
      }
    }
  }

  private class CodeFormToken {

    private final StringBuilder code = new StringBuilder();
    private final List commentsInside;
    private int lastLine = 1;
    private int lastLineOffset = 0;
    private int lastComment = 0;

    private CodeFormToken(TreeMetaData metaData) {
      this.commentsInside = metaData.commentsInside();
      metaData.tokens().forEach(this::add);
      addRemainingComments();
    }

    private void add(Token token) {
      while (lastComment < commentsInside.size() &&
        commentsInside.get(lastComment).textRange().start().compareTo(token.textRange().start()) < 0) {
        Comment comment = commentsInside.get(lastComment);
        addTextAt(comment.text(), comment.textRange());
        lastComment++;
      }
      addTextAt(token.text(), token.textRange());
    }

    private void addRemainingComments() {
      for (int i = lastComment; i < commentsInside.size(); i++) {
        addTextAt(commentsInside.get(i).text(), commentsInside.get(i).textRange());
      }
    }

    private void addTextAt(String text, TextRange textRange) {
      while (lastLine < textRange.start().line()) {
        code.append("\n");
        lastLine++;
        lastLineOffset = 0;
      }
      while (lastLineOffset < textRange.start().lineOffset()) {
        code.append(' ');
        lastLineOffset++;
      }
      code.append(text);
      lastLine = textRange.end().line();
      lastLineOffset = textRange.end().lineOffset();
    }

    private void assertEqualTo(String expectedCode) {
      String[] actualLines = lines(this.code.toString());
      String[] expectedLines = lines(expectedCode);
      for (int i = 0; i < actualLines.length && i < expectedLines.length; i++) {
        if (!actualLines[i].equals(expectedLines[i])) {
          raiseError("Unexpected AST difference", ":\n" +
            "      Actual   : " + actualLines[i] + "\n" +
            "      Expected : " + expectedLines[i] + "\n",
            new TextPointerImpl(i + 1, 0));
        }
      }
      if (actualLines.length != expectedLines.length) {
        raiseError(
          "Unexpected AST number of lines",
          " actual: " + actualLines.length + ", expected: " + expectedLines.length,
          new TextPointerImpl(Math.min(actualLines.length, expectedLines.length), 0));
      }
    }

    private String[] lines(String code) {
      return code
        .replace('\t', ' ')
        .replaceFirst("[\r\n ]+$", "")
        .split(" *(\r\n|\n|\r)", -1);
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy