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

org.sonar.java.checks.DateTimeFormatterMismatchCheck Maven / Gradle / Ivy

/*
 * SonarQube Java
 * Copyright (C) 2012-2024 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 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.checks;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S5917")
public class DateTimeFormatterMismatchCheck extends IssuableSubscriptionVisitor {
  private static final String TEMPORAL_FIELD_TYPE = "java.time.temporal.TemporalField";
  private static final String INT_TYPE = "int";

  private static final MethodMatchers APPEND_VALUE_MATCHER = MethodMatchers.create()
    .ofTypes("java.time.format.DateTimeFormatterBuilder")
    .names("appendValue")
    .addParametersMatcher(TEMPORAL_FIELD_TYPE)
    .addParametersMatcher(TEMPORAL_FIELD_TYPE, INT_TYPE)
    .addParametersMatcher(TEMPORAL_FIELD_TYPE, INT_TYPE, INT_TYPE, "java.time.format.SignStyle")
    .build();

  private static final String CHANGE_YEAR_FORMAT_WEEK_BASED_MESSAGE = "Change this year format to use the week-based year instead" +
    " (or the week format to Chronofield.ALIGNED_WEEK_OF_YEAR).";
  private static final String CHANGE_YEAR_FORMAT_TO_CHRONOFIELD_MESSAGE = "Change this year format to use ChronoField.YEAR instead" +
    " (or the week format to WeekFields.ISO.weekOfWeekBasedYear()).";
  private static final String SECONDARY_LOCATION_MESSAGE = "Week format is inconsistent with year.";

  private int depth = 0;

  @Override
  public List nodesToVisit() {
    return Collections.singletonList(Tree.Kind.METHOD_INVOCATION);
  }

  @Override
  public void visitNode(Tree tree) {
    MethodInvocationTree invocation = (MethodInvocationTree) tree;
    if (APPEND_VALUE_MATCHER.matches(invocation)) {
      if (depth == 0) {
        ChainedInvocationVisitor visitor = new ChainedInvocationVisitor();
        invocation.accept(visitor);
        if (visitor.usesWeek && visitor.usesYear) {
          if (visitor.usesWeekBasedYear && !visitor.usesWeekOfWeekBasedYear) {
            reportIssue(visitor.primary, CHANGE_YEAR_FORMAT_TO_CHRONOFIELD_MESSAGE, visitor.secondaries, null);
          } else if (!visitor.usesWeekBasedYear && visitor.usesWeekOfWeekBasedYear) {
            reportIssue(visitor.primary, CHANGE_YEAR_FORMAT_WEEK_BASED_MESSAGE, visitor.secondaries, null);
          }
        }
      }
      depth++;
    }
  }

  @Override
  public void leaveNode(Tree tree) {
    if (APPEND_VALUE_MATCHER.matches((MethodInvocationTree) tree)) {
      depth--;
    }
  }

  @Override
  public void leaveFile(JavaFileScannerContext context) {
    depth = 0;
  }


  private static class ChainedInvocationVisitor extends BaseTreeVisitor {
    private static final MethodMatchers WEEK_BASED_YEAR_MATCHER = MethodMatchers.create()
      .ofTypes("java.time.temporal.WeekFields")
      .names("weekBasedYear")
      .addWithoutParametersMatcher()
      .build();

    private static final MethodMatchers WEEK_OF_WEEK_BASED_YEAR_MATCHER = MethodMatchers.create()
      .ofTypes("java.time.temporal.WeekFields")
      .names("weekOfWeekBasedYear")
      .addWithoutParametersMatcher()
      .build();

    private boolean usesWeek = false;
    private boolean usesWeekOfWeekBasedYear = false;
    private boolean usesYear = false;
    private boolean usesWeekBasedYear = false;
    private final List secondaries = new ArrayList<>();
    private ExpressionTree primary = null;

    @Override
    public void visitMethodInvocation(MethodInvocationTree tree) {
      inspectCall(tree);
      ExpressionTree expressionTree = tree.methodSelect();
      expressionTree.accept(this);
    }

    private void inspectCall(MethodInvocationTree invocation) {
      if (!APPEND_VALUE_MATCHER.matches(invocation)) {
        return;
      }
      ExpressionTree argument = invocation.arguments().get(0);
      if (refersToYear(argument)) {
        usesYear = true;
        if (primary == null) {
          primary = argument;
        }
        boolean isWeekBasedYearArgument = isWeekBasedYearUsed(argument);
        usesWeekBasedYear |= isWeekBasedYearArgument;
        if (isWeekBasedYearArgument) {
          primary = argument;
        }
      } else if (refersToWeek(argument)) {
        usesWeek = true;
        secondaries.add(new JavaFileScannerContext.Location(SECONDARY_LOCATION_MESSAGE, argument));
        usesWeekOfWeekBasedYear |= isWeekOfWeekBasedYearUsed(argument);
      }
    }

    private static boolean refersToWeek(ExpressionTree argument) {
      return isChronoFieldWeek(argument) || isWeekOfWeekBasedYearUsed(argument);
    }

    private static boolean isWeekOfWeekBasedYearUsed(ExpressionTree argument) {
      if (argument.is(Tree.Kind.METHOD_INVOCATION)) {
        MethodInvocationTree call = (MethodInvocationTree) argument;
        return WEEK_OF_WEEK_BASED_YEAR_MATCHER.matches(call);
      }
      return false;
    }

    private static boolean isChronoFieldWeek(ExpressionTree argument) {
      if (!argument.is(Tree.Kind.MEMBER_SELECT)) {
        return false;
      }
      MemberSelectExpressionTree select = (MemberSelectExpressionTree) argument;
      if (!select.symbolType().is("java.time.temporal.ChronoField")) {
        return false;
      }
      return "ALIGNED_WEEK_OF_YEAR".equals(select.identifier().name());
    }

    private static boolean refersToYear(ExpressionTree argument) {
      return isChronoFieldYear(argument) || isWeekBasedYearUsed(argument);
    }

    private static boolean isWeekBasedYearUsed(ExpressionTree argument) {
      if (argument.is(Tree.Kind.METHOD_INVOCATION)) {
        MethodInvocationTree call = (MethodInvocationTree) argument;
        return WEEK_BASED_YEAR_MATCHER.matches(call);
      }
      return false;
    }

    private static boolean isChronoFieldYear(ExpressionTree argument) {
      if (argument.is(Tree.Kind.MEMBER_SELECT)) {
        MemberSelectExpressionTree select = (MemberSelectExpressionTree) argument;
        String name = select.identifier().name();
        return select.symbolType().is("java.time.temporal.ChronoField") && ("YEAR".equals(name) || "YEAR_OF_ERA".equals(name));
      }
      return false;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy