QueryCompat.java

package org.jsoup.select;

import org.jsoup.internal.StringUtil;
import org.jsoup.nodes.Element;
import org.jspecify.annotations.Nullable;

import java.util.Iterator;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 Runs queries in Web mode to report compatibility differences, while callers retain legacy results.
 Scheduled for removal with check mode in 1.26.1.
 */
final class QueryCompat {
    private QueryCompat() {}

    /**
     Checks that collection results contain the same elements in the same order.
     */
    static void check(String query, Elements legacy, Function<Evaluator, Elements> execute) {
        check("select", query, evaluator -> {
            Elements web = execute.apply(evaluator);
            return sameResults(legacy, web) ? null :
                String.format("legacy=%s, web=%s; results differ", legacy.size(), web.size());
        });
    }

    /**
     Checks a single-element result by identity, including null results.
     */
    static void check(String operation, String query, @Nullable Element legacy, Function<Evaluator, @Nullable Element> execute) {
        check(operation, query, evaluator -> legacy == execute.apply(evaluator) ? null : "results differ");
    }

    /**
     Checks whether both modes agree that an element matches.
     */
    static void check(String query, boolean legacy, Predicate<Evaluator> execute) {
        check("is", query, evaluator -> {
            boolean web = execute.test(evaluator);
            return legacy == web ? null : String.format("legacy=%s, web=%s", legacy, web);
        });
    }

    /**
     Runs a Web query comparison, reporting non-null difference details or query errors.
     */
    private static void check(String operation, String query, Function<Evaluator, @Nullable String> compare) {
        Evaluator web;
        try {
            web = QueryParser.parse(query, true);
        } catch (RuntimeException e) {
            warn(operation, query, String.format("web parse failed: %s", e.getMessage()));
            return;
        }
        String detail;
        try {
            detail = compare.apply(web);
        } catch (RuntimeException e) {
            warn(operation, query, String.format("web evaluation failed: %s", e.getMessage()));
            return;
        }
        if (detail != null) warn(operation, query, detail);
    }

    /**
     Compares collections by element identity and order.
     */
    private static boolean sameResults(Elements legacy, Elements web) {
        Iterator<Element> first = legacy.iterator();
        Iterator<Element> second = web.iterator();
        while (first.hasNext() && second.hasNext()) {
            if (first.next() != second.next()) return false;
        }
        return !first.hasNext() && !second.hasNext();
    }

    /**
     Prints a single-line warning with up to two caller locations.
     */
    private static void warn(String operation, String query, String detail) {
        StringUtil.StringJoiner message = new StringUtil.StringJoiner("; ")
            .add(String.format("jsoup query compat: %s(\"%s\"): %s", operation, query, detail));
        int callers = 0;
        for (StackTraceElement frame : new Throwable().getStackTrace()) {
            if (callers == 0 && isInternalFrame(frame.getClassName())) continue;
            message.add("at ").append(frame);
            if (++callers == 2) break;
        }
        System.err.println(StringUtil.normaliseWhitespace(message.complete()));
    }

    /**
     Identifies query implementation classes, allowing callers elsewhere in org.jsoup to appear in warnings.
     */
    private static boolean isInternalFrame(String name) {
        for (Class<?> type : InternalClasses) {
            String className = type.getName();
            if (name.equals(className) || name.startsWith(className + "$")) return true;
        }
        return false;
    }

    private static final Class<?>[] InternalClasses = {
        QueryCompat.class, Selector.class, EvaluatorInternals.class, Element.class, Elements.class
    };
}