From 9722cf53a82f50c130885cde7b37c06d2cba0c8c Mon Sep 17 00:00:00 2001 From: Alexander Bashkirov Date: Sun, 3 Oct 2021 17:38:01 +0300 Subject: [PATCH] [util] Implemented runtime invariants checking facility GitOrigin-RevId: 7c32d60235a46f4d35807ac744bc6c1109d31084 --- .../intellij/openapi/diagnostic/Checks.java | 574 ++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 platform/util/src/com/intellij/openapi/diagnostic/Checks.java diff --git a/platform/util/src/com/intellij/openapi/diagnostic/Checks.java b/platform/util/src/com/intellij/openapi/diagnostic/Checks.java new file mode 100644 index 000000000000..2fbefa6e937f --- /dev/null +++ b/platform/util/src/com/intellij/openapi/diagnostic/Checks.java @@ -0,0 +1,574 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.diagnostic; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + + +/** + * Runtime invariants checking facilities. Provides the simplest form of the + * contract programming. + * + *

There are three groups of methods: + *

    + *
  1. Preconditions (input checks) via {@code require} methods. Those methods should be used to + * check system arguments and other input data. + *
  2. + *
  3. Invariants (state checks) via {@code check} methods. Those methods should be used to + * check system internal state or some predicates which are expected to be satisfied at the call site. + *
  4. + *
  5. Postconditions (output checks) via {@code ensure} methods. Those methods should be used to + * check system results and other output data. + *
  6. + *
+ *

+ * + *

Each method contains counterpart with {@code andLog} suffix -- those methods only log errors + * and do not throw exception, i.e do not break the control flow. + * We suggest using overloads with {@link Attachment} provided, it may significantly reduce error investigation time.

+ * + *

NOTE: for performance critical parts of code of for the checks + * which should not be enabled under some circumstances it is likely better to use standard Java assertions + * via {@code assert statement : message} construction.

+ */ +@ApiStatus.Experimental +public class Checks { + + public static final @NotNull @NonNls String PRECONDITION_IS_NOT_SATISFIED = "Precondition is not satisfied"; + public static final @NotNull @NonNls String INVARIANT_IS_NOT_SATISFIED = "Invariant is not satisfied"; + public static final @NotNull @NonNls String POSTCONDITION_IS_NOT_SATISFIED = "Postcondition is not satisfied"; + + private Checks() { + } + + /* ------------------------------------------------------------------------------------------- */ + //region Preconditions, input assertions + + /** + * @throws IllegalArgumentException if the {@code statement} is {@code false} + */ + @Contract("false -> fail") + public static void require(boolean statement) { + if (!statement) { + throw new IllegalArgumentException(PRECONDITION_IS_NOT_SATISFIED); + } + } + + /** + * @throws IllegalArgumentException with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void require(boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + throw new IllegalArgumentException(lazyMessage.get().toString()); + } + } + + /** + * @throws IllegalArgumentException with the given {@code message} if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void require(boolean statement, @NotNull String message) { + if (!statement) { + throw new IllegalArgumentException(message); + } + } + + /** + * @throws IllegalArgumentException with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void require(@NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalArgumentException(message); + } + } + + /** + * @throws IllegalArgumentException with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void require(@NotNull Supplier lazyMessage, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalArgumentException(lazyMessage.get().toString()); + } + } + + /** + * Logs {@link IllegalArgumentException} if the {@code statement} is {@code false} + */ + public static void requireAndLog(@NotNull Class loggingClass, boolean statement) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalArgumentException(PRECONDITION_IS_NOT_SATISFIED)); + } + } + + /** + * Logs {@link IllegalArgumentException} with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + public static void requireAndLog(@NotNull Class loggingClass, boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalArgumentException(lazyMessage.get().toString())); + } + } + + /** + * Logs {@link IllegalArgumentException} with the given {@code message} if the {@code statement} is {@code false}. + */ + public static void requireAndLog(@NotNull Class loggingClass, boolean statement, @NotNull String message) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalArgumentException(message)); + } + } + + /** + * Logs {@link IllegalArgumentException} with the given {@code message} and {@code attachments} if the {@code statement} is {@code false}. + */ + public static void requireAndLog( + @NotNull Class loggingClass, + boolean statement, + @NotNull String message, + Attachment @NotNull ... attachments + ) { + if (!statement) { + Logger.getInstance(loggingClass).error(message, new IllegalArgumentException(message), attachments); + } + } + + /** + * Logs {@link IllegalArgumentException} with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void requireAndLog(@NotNull Class loggingClass, @NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalArgumentException(message)); + } + } + + /** + * Logs {@link IllegalArgumentException} with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void requireAndLog( + @NotNull Class loggingClass, + @NotNull Supplier lazyMessage, + @NotNull BooleanSupplier statementSupplier + ) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalArgumentException(lazyMessage.get().toString())); + } + } + + //endregion + /* ------------------------------------------------------------------------------------------- */ + + + /* ------------------------------------------------------------------------------------------- */ + //region Invariants, state assertions + + /** + * @throws IllegalStateException if the {@code statement} is {@code false} + */ + @Contract("false -> fail") + public static void check(boolean statement) { + if (!statement) { + throw new IllegalStateException(INVARIANT_IS_NOT_SATISFIED); + } + } + + /** + * @throws IllegalStateException with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void check(boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + throw new IllegalStateException(lazyMessage.get().toString()); + } + } + + /** + * @throws IllegalStateException with the given {@code message} if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void check(boolean statement, @NotNull String message) { + if (!statement) { + throw new IllegalStateException(message); + } + } + + /** + * @throws IllegalStateException with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void check(@NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalStateException(message); + } + } + + /** + * @throws IllegalStateException with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void check(@NotNull Supplier lazyMessage, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalStateException(lazyMessage.get().toString()); + } + } + + /** + * Logs {@link IllegalStateException} if the {@code statement} is {@code false} + */ + public static void checkAndLog(@NotNull Class loggingClass, boolean statement) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalStateException(INVARIANT_IS_NOT_SATISFIED)); + } + } + + /** + * Logs {@link IllegalStateException} with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + public static void checkAndLog(@NotNull Class loggingClass, boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalStateException(lazyMessage.get().toString())); + } + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} if the {@code statement} is {@code false}. + */ + public static void checkAndLog(@NotNull Class loggingClass, boolean statement, @NotNull String message) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalStateException(message)); + } + } + + + /** + * Logs {@link IllegalStateException} with the given {@code message} and {@code attachments} if the {@code statement} is {@code false}. + */ + public static void checkAndLog( + @NotNull Class loggingClass, + boolean statement, + @NotNull String message, + Attachment @NotNull ... attachments + ) { + if (!statement) { + Logger.getInstance(loggingClass).error(message, new IllegalStateException(message), attachments); + } + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void checkAndLog(@NotNull Class loggingClass, @NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalStateException(message)); + } + } + + /** + * Logs {@link IllegalStateException} with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void checkAndLog( + @NotNull Class loggingClass, + @NotNull Supplier lazyMessage, + @NotNull BooleanSupplier statementSupplier + ) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalStateException(lazyMessage.get().toString())); + } + } + + + /** + * Ensures that {@code index} specifies a valid element in an array, collection or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, collection or string + * @param size the size of that array, collection or string + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + * @apiNote Guava's {@code com.google.common.base.Preconditions#checkElementIndex(int, int)} analogue. + */ + public static void checkIndex(int index, int size) { + checkIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid element in an array, collection or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, collection or string + * @param size the size of that array, collection or string + * @param indexDescription the text to use to describe this index in an error message + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + * @apiNote Guava's {@code com.google.common.base.Preconditions#checkElementIndex(int, int, java.lang.String)} analogue. + */ + public static void checkIndex(int index, int size, @NotNull String indexDescription) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(badElementIndex(index, size, indexDescription)); + } + } + + /** + * Analogue of the {@link #checkIndex(int, int)} which logs instead of throwing an error. + */ + public static void checkIndexAndLog(@NotNull Class loggingClass, int index, int size) { + checkIndexAndLog(loggingClass, index, size, "index"); + } + + /** + * Analogue of the {@link #checkIndex(int, int, String)} which logs instead of throwing an error. + */ + public static void checkIndexAndLog(@NotNull Class loggingClass, int index, int size, @NotNull String indexDescription) { + if (index < 0 || index >= size) { + Logger.getInstance(loggingClass).error(new IndexOutOfBoundsException(badElementIndex(index, size, indexDescription))); + } + } + + /** + * Analogue of the {@link #checkIndex(int, int, String)} which logs with {@code attachments} provided instead of throwing an error. + */ + public static void checkIndexAndLog( + @NotNull Class loggingClass, + int index, + int size, + @NotNull String indexDescription, + Attachment @NotNull ... attachments + ) { + if (index < 0 || index >= size) { + String message = badElementIndex(index, size, indexDescription); + Logger.getInstance(loggingClass).error(message, new IndexOutOfBoundsException(message), attachments); + } + } + + private static @NotNull String badElementIndex(int index, int size, @NotNull String indexDescription) { + if (index < 0) { + return indexDescription + " (" + index + ") must not be negative"; + } + else if (size < 0) { + throw new IllegalArgumentException("negative size: " + size); + } + else { // index >= size + return indexDescription + " (" + index + ") must be less than size (" + size + ")"; + } + } + + //endregion + /* ------------------------------------------------------------------------------------------- */ + + + /* ------------------------------------------------------------------------------------------- */ + //region Postconditions, output assertions + + /** + * @throws IllegalPostconditionException if the {@code statement} is {@code false} + */ + @Contract("false -> fail") + public static void ensure(boolean statement) { + if (!statement) { + throw new IllegalPostconditionException(POSTCONDITION_IS_NOT_SATISFIED); + } + } + + /** + * @throws IllegalPostconditionException with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void ensure(boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + throw new IllegalPostconditionException(lazyMessage.get().toString()); + } + } + + /** + * @throws IllegalPostconditionException with the given {@code message} if the {@code statement} is {@code false}. + */ + @Contract("false, _ -> fail") + public static void ensure(boolean statement, @NotNull String message) { + if (!statement) { + throw new IllegalPostconditionException(message); + } + } + + /** + * @throws IllegalPostconditionException with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void ensure(@NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalPostconditionException(message); + } + } + + /** + * @throws IllegalPostconditionException with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void ensure(@NotNull Supplier lazyMessage, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + throw new IllegalPostconditionException(lazyMessage.get().toString()); + } + } + + /** + * Logs {@link IllegalPostconditionException} if the {@code statement} is {@code false} + */ + public static void ensureAndLog(@NotNull Class loggingClass, boolean statement) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalPostconditionException(POSTCONDITION_IS_NOT_SATISFIED)); + } + } + + /** + * Logs {@link IllegalPostconditionException} with the result of calling {@code lazyMessage} + * if the {@code statement} is {@code false}. + */ + public static void ensureAndLog(@NotNull Class loggingClass, boolean statement, @NotNull Supplier lazyMessage) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalPostconditionException(lazyMessage.get().toString())); + } + } + + /** + * Logs {@link IllegalPostconditionException} with the given {@code message} if the {@code statement} is {@code false}. + */ + public static void ensureAndLog(@NotNull Class loggingClass, boolean statement, @NotNull String message) { + if (!statement) { + Logger.getInstance(loggingClass).error(new IllegalPostconditionException(message)); + } + } + + /** + * Logs {@link IllegalPostconditionException} with the given {@code message} and {@code attachments} + * if the {@code statement} is {@code false}. + */ + public static void ensureAndLog( + @NotNull Class loggingClass, + boolean statement, + @NotNull String message, + Attachment @NotNull ... attachments + ) { + if (!statement) { + Logger.getInstance(loggingClass).error(message, new IllegalPostconditionException(message), attachments); + } + } + + /** + * Logs {@link IllegalPostconditionException} with the given {@code message} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void ensureAndLog(@NotNull Class loggingClass, @NotNull String message, @NotNull BooleanSupplier statementSupplier) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalPostconditionException(message)); + } + } + + /** + * Logs {@link IllegalPostconditionException} with the result of calling {@code lazyMessage} + * if the {@code statementSupplier.getAsBoolean()} is {@code false}. + */ + public static void ensureAndLog( + @NotNull Class loggingClass, + @NotNull Supplier lazyMessage, + @NotNull BooleanSupplier statementSupplier + ) { + if (!statementSupplier.getAsBoolean()) { + Logger.getInstance(loggingClass).error(new IllegalPostconditionException(lazyMessage.get().toString())); + } + } + + //endregion + /* ------------------------------------------------------------------------------------------- */ + + + /* ------------------------------------------------------------------------------------------- */ + //region Error throwing / logging + + /** + * @throws IllegalStateException with the given {@code message}. + */ + @Contract("_ -> fail") + public static void fail(@NotNull Object message) { + throw new IllegalStateException(message.toString()); + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} and {@code attachments} as an error. + */ + public static void logError(@NotNull Class loggingClass, @NotNull Object message, Attachment @NotNull ... attachments) { + String s = message.toString(); + Logger.getInstance(loggingClass).error(s, new IllegalStateException(s), attachments); + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} as a warning. + */ + public static void logWarn(@NotNull Class loggingClass, @NotNull Object message) { + Logger.getInstance(loggingClass).warn(new IllegalStateException(message.toString())); + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} as debug info. + */ + public static void logDebug(@NotNull Class loggingClass, @NotNull Object message) { + Logger.getInstance(loggingClass).debug(new IllegalStateException(message.toString())); + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} as info. + */ + public static void logInfo(@NotNull Class loggingClass, @NotNull Object message) { + Logger.getInstance(loggingClass).info(new IllegalStateException(message.toString())); + } + + /** + * Logs {@link IllegalStateException} with the given {@code message} as trace info. + */ + public static void logTrace(@NotNull Class loggingClass, @NotNull Object message) { + Logger.getInstance(loggingClass).trace(new IllegalStateException(message.toString())); + } + + //endregion + /* ------------------------------------------------------------------------------------------- */ + + + /** + * Indicates that some postcondition or invariant over results and other output values was not satisfied. + * It is semantically close to {@link IllegalArgumentException} but for method results. + */ + public static final class IllegalPostconditionException extends RuntimeException { + public IllegalPostconditionException() { + } + + public IllegalPostconditionException(String message) { + super(message); + } + + public IllegalPostconditionException(String message, Throwable cause) { + super(message, cause); + } + + public IllegalPostconditionException(Throwable cause) { + super(cause); + } + + public IllegalPostconditionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } +}