/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java.se; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import org.apache.commons.lang.StringUtils; import org.assertj.core.api.Fail; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.java.AnalyzerMessage; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.tree.SyntaxTrivia; import org.sonar.plugins.java.api.tree.Tree; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Scanner; import java.util.Set; import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.sonar.java.se.Expectations.IssueAttribute.EFFORT_TO_FIX; import static org.sonar.java.se.Expectations.IssueAttribute.END_COLUMN; import static org.sonar.java.se.Expectations.IssueAttribute.END_LINE; import static org.sonar.java.se.Expectations.IssueAttribute.FLOWS; import static org.sonar.java.se.Expectations.IssueAttribute.LINE; import static org.sonar.java.se.Expectations.IssueAttribute.MESSAGE; import static org.sonar.java.se.Expectations.IssueAttribute.ORDER; import static org.sonar.java.se.Expectations.IssueAttribute.SECONDARY_LOCATIONS; import static org.sonar.java.se.Expectations.IssueAttribute.START_COLUMN; class Expectations { private static final Logger LOG = Loggers.get(Expectations.class); private static final Map<String, IssueAttribute> ATTRIBUTE_MAP = ImmutableMap.<String, IssueAttribute>builder() .put("message", MESSAGE) .put("effortToFix", EFFORT_TO_FIX) .put("sc", START_COLUMN) .put("startColumn", START_COLUMN) .put("el", END_LINE) .put("endLine", END_LINE) .put("ec", END_COLUMN) .put("endColumn", END_COLUMN) .put("secondary", SECONDARY_LOCATIONS) .put("flows", FLOWS) .put("order", ORDER) .build(); enum IssueAttribute { LINE(Function.identity()), ORDER(Integer::valueOf), MESSAGE(Function.identity()), START_COLUMN(Integer::valueOf), END_COLUMN(Integer::valueOf), END_LINE(Parser.LineRef::fromString, Parser.LineRef::toLine), EFFORT_TO_FIX(Double::valueOf), SECONDARY_LOCATIONS(multiValueAttribute(Integer::valueOf)), FLOWS(multiValueAttribute(Function.identity())); private Function<String, ?> setter; Function<Object, Object> getter = Function.identity(); IssueAttribute(Function<String, ?> setter) { this.setter = setter; } IssueAttribute(Function<String, ?> setter, Function<Object, Object> getter) { this.setter = setter; this.getter = getter; } static <T> Function<String, List<T>> multiValueAttribute(Function<String, T> convert) { return (String input) -> Strings.isNullOrEmpty(input) ? Collections.emptyList() : Arrays.stream(input.split(",")).map(convert).collect(toList()); } <T> T get(Map<IssueAttribute, Object> values) { Object rawValue = values.get(this); return rawValue == null ? null : (T) getter.apply(rawValue); } } static class Issue extends EnumMap<IssueAttribute, Object> { private Issue() { super(IssueAttribute.class); } static Issue create() { return new Issue(); } } static class FlowComment { final String id; final int line; final Map<IssueAttribute, Object> attributes; public FlowComment(String id, int line, Map<IssueAttribute, Object> attributes) { this.id = id; this.line = line; this.attributes = Collections.unmodifiableMap(attributes); } <T> T get(IssueAttribute attribute) { return (T) attribute.get(attributes); } int order() { Integer order = ORDER.get(attributes); return order == null ? 0 : order; } @CheckForNull String message() { return MESSAGE.get(attributes); } @Override public String toString() { return String.format("%d: flow@%s %s", line, id, attributes.toString()); } } final Multimap<Integer, Issue> issues = ArrayListMultimap.create(); final ListMultimap<String, FlowComment> flows = ArrayListMultimap.create(); final boolean expectNoIssues; final String expectFileIssue; final Integer expectFileIssueOnLine; //initialized lazily in #containFlow private Map<ImmutableList<Integer>, String> flowLines = null; private Set<String> seenFlowIds = new HashSet<>(); Expectations() { this(false, null, null); } Expectations(boolean expectNoIssues, @Nullable String expectFileIssue, @Nullable Integer expectFileIssueOnLine) { this.expectNoIssues = expectNoIssues; this.expectFileIssue = expectFileIssue; this.expectFileIssueOnLine = expectFileIssueOnLine; } void reverseFlows() { Multimaps.asMap(flows).forEach((id, flow) -> Collections.reverse(flow)); } Optional<String> containFlow(List<AnalyzerMessage> flow) { if (flowLines == null) { flowLines = computeFlowLines(); } ImmutableList<Integer> actualLines = flowToLines(flow, AnalyzerMessage::getLine); Optional<String> flowId = Optional.ofNullable(flowLines.get(actualLines)); flowId.ifPresent(id -> seenFlowIds.add(id)); return flowId; } Set<String> unseenFlowIds() { return Sets.difference(flows.keySet(), seenFlowIds); } private Map<ImmutableList<Integer>, String> computeFlowLines() { Map<String, List<FlowComment>> flows = Multimaps.asMap(this.flows); flows.forEach((id, flow) -> flow.sort(Comparator.comparingInt(FlowComment::order))); return flows.entrySet().stream() .collect(Collectors.toMap(e -> flowToLines(e.getValue(), flowComment -> flowComment.line), Map.Entry::getKey)); } private static <T> ImmutableList<Integer> flowToLines(Collection<T> flow, ToIntFunction<T> toLineFunction) { return flow.stream() .mapToInt(toLineFunction) .sorted().boxed() .collect(collectingAndThen(toList(), ImmutableList::copyOf)); } String flowToLines(String flowId) { return flows.get(flowId).stream().map(f -> String.valueOf(f.line)).collect(joining(",")); } IssuableSubscriptionVisitor parser() { return new Parser(issues, flows); } @VisibleForTesting static class Parser extends IssuableSubscriptionVisitor { private static final Pattern NONCOMPLIANT_COMMENT = Pattern.compile("//\\s+Noncompliant"); private static final Pattern FLOW_COMMENT = Pattern.compile("//\\s+flow"); private static final Pattern SHIFT = Pattern.compile("Noncompliant@(\\S+)"); private static final Pattern FLOW = Pattern.compile("flow@(?<ids>\\S+).*?(?=flow@)?"); private final Multimap<String, FlowComment> flows; private final Multimap<Integer, Issue> issues; Parser(Multimap<Integer, Issue> issues, Multimap<String, FlowComment> flows) { this.issues = issues; this.flows = flows; } @Override public List<Tree.Kind> nodesToVisit() { return ImmutableList.of(Tree.Kind.TRIVIA); } @Override public void visitTrivia(SyntaxTrivia syntaxTrivia) { collectExpectedIssues(syntaxTrivia.comment(), syntaxTrivia.startLine()); } @VisibleForTesting void collectExpectedIssues(String comment, int line) { if (NONCOMPLIANT_COMMENT.matcher(comment).find()) { ParsedComment parsedComment = parseIssue(comment, line); issues.put(LINE.get(parsedComment.issue), parsedComment.issue); parsedComment.flows.forEach(f -> flows.put(f.id, f)); } if (FLOW_COMMENT.matcher(comment).find()) { parseFlows(comment, line).forEach(f -> flows.put(f.id, f)); } } @VisibleForTesting static List<FlowComment> parseFlows(@Nullable String comment, int line) { if (comment == null) { return Collections.emptyList(); } List<List<String>> flowIds = new ArrayList<>(); List<Integer> flowStarts = new ArrayList<>(); Matcher matcher = FLOW.matcher(comment); while (matcher.find()) { List<String> ids = Arrays.asList(matcher.group("ids").split(",")); flowIds.add(ids); flowStarts.add(matcher.start()); } // add one more fake start at the end, so the boundary of comment i is flowStarts[i],flowStarts[i+1] also for the last one flowStarts.add(comment.length()); return IntStream.range(0, flowIds.size()) .mapToObj(i -> createFlows(flowIds.get(i), line, comment.substring(flowStarts.get(i), flowStarts.get(i + 1)))) .flatMap(Function.identity()) .collect(Collectors.toList()); } private static Stream<FlowComment> createFlows(List<String> ids, int line, String flow) { Map<IssueAttribute, Object> attributes = new EnumMap<>(IssueAttribute.class); attributes.putAll(parseAttributes(flow)); String message = parseMessage(flow, flow.length()); attributes.put(MESSAGE, message); return ids.stream().map(id -> new FlowComment(id, line, attributes)); } @VisibleForTesting static ParsedComment parseIssue(String comment, int line) { Matcher shiftMatcher = SHIFT.matcher(comment); Matcher flowMatcher = FLOW.matcher(comment); return createIssue(line, shiftMatcher.find() ? shiftMatcher.group(1) : null, comment, parseMessage(comment, flowMatcher.find() ? flowMatcher.start() : comment.length()), comment); } private static ParsedComment createIssue(int line, @Nullable String shift, @Nullable String attributes, @Nullable String message, @Nullable String flow) { Issue issue = Issue.create(); issue.put(LINE, parseLineShifting(shift).getLine(line)); Map<IssueAttribute, Object> attrs = parseAttributes(attributes); attrs = adjustEndLine(attrs, line); issue.putAll(attrs); if (message != null) { issue.put(MESSAGE, message); } List<FlowComment> flows = parseFlows(flow, line); return new ParsedComment(issue, flows); } private static LineRef parseLineShifting(@Nullable String shift) { if (shift == null) { return new LineRef.RelativeLineRef(0); } try { return LineRef.fromString(shift); } catch (NumberFormatException e) { Fail.fail("Use only '@+N' or '@-N' to shifts messages."); return null; } } private static Map<IssueAttribute, Object> parseAttributes(@Nullable String comment) { comment = StringUtils.substringBetween(comment, "[[", "]]"); if (comment == null) { return Collections.emptyMap(); } return Arrays.stream(comment.split(";")) .map(Parser::parseAttribute) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } private static Map<IssueAttribute, Object> adjustEndLine(Map<IssueAttribute, Object> attributes, int line) { Object endLine = attributes.get(END_LINE); if (endLine != null && endLine instanceof LineRef.RelativeLineRef) { LineRef.RelativeLineRef relativeLineRef = (LineRef.RelativeLineRef) endLine; if (relativeLineRef.offset < 0) { Fail.fail("endLine attribute should be relative to the line and must be +N with N integer"); } EnumMap<IssueAttribute, Object> copy = new EnumMap<>(attributes); copy.put(END_LINE, new LineRef.AbsoluteLineRef(relativeLineRef.getLine(line))); return copy; } return attributes; } private static Map.Entry<IssueAttribute, Object> parseAttribute(String attribute) { Scanner scanner = new Scanner(attribute).useDelimiter("[=]+"); String name = scanner.next(); if (!ATTRIBUTE_MAP.containsKey(name)) { Fail.fail("// Noncompliant attributes not valid: " + attribute); } IssueAttribute key = ATTRIBUTE_MAP.get(name); Object value = key.setter.apply(scanner.hasNext() ? scanner.next() : null); return new AbstractMap.SimpleImmutableEntry<>(key, value); } private static String parseMessage(String cleanedComment, int horizon) { return StringUtils.substringBetween(cleanedComment.substring(0, horizon), "{{", "}}"); } static class ParsedComment { final Issue issue; final List<FlowComment> flows; private ParsedComment(Issue issue, List<FlowComment> flows) { this.issue = issue; this.flows = flows; } } abstract static class LineRef { abstract int getLine(int ref); static LineRef fromString(String input) { if (input.startsWith("+") || input.startsWith("-")) { return new RelativeLineRef(Integer.valueOf(input)); } else { return new AbsoluteLineRef(Integer.valueOf(input)); } } static int toLine(Object ref) { return ((LineRef) ref).getLine(0); } static class AbsoluteLineRef extends LineRef { final int line; public AbsoluteLineRef(int line) { this.line = line; } public int getLine(int ref) { return line; } } static class RelativeLineRef extends LineRef { final int offset; RelativeLineRef(int offset) { this.offset = offset; } @Override int getLine(int ref) { return ref + offset; } } @Override public int hashCode() { return Objects.hash(getLine(0)); } @Override public boolean equals(Object obj) { return LineRef.class.isAssignableFrom(obj.getClass()) && Objects.equals(getLine(0), ((LineRef) obj).getLine(0)); } } } }