PathStems.java

package org.egothor.methodatlas.util;

import java.nio.file.Path;

/**
 * Computes the dot-separated "file stem" (and fully-qualified grouping name)
 * used by discovery plugins to identify a source file relative to the scan
 * root.
 *
 * <p>
 * Historically each plugin re-implemented the same three steps — relativise the
 * file against the root, join the path segments with {@code '.'}, and strip the
 * file-name suffix from the final segment — differing only in which suffix to
 * strip.  This helper is the single canonical implementation.
 * </p>
 *
 * <p>
 * Both paths are converted with {@link Path#toAbsolutePath()} then
 * {@link Path#normalize()} before relativising, so callers may pass either
 * absolute or relative paths as long as both are the same kind.  The suffix is
 * stripped from the last <em>segment</em> only, so intermediate directory names
 * that contain dots are never mistaken for an extension boundary.
 * </p>
 */
public final class PathStems {

    private PathStems() {
        // utility class — no instances
    }

    /**
     * Returns the dot-separated stem of {@code file} relative to {@code root}
     * with a trailing file-name suffix removed from the last segment.
     *
     * <p>
     * The <em>longest</em> matching suffix from {@code suffixesToStrip} is
     * removed (so {@code ".test.ts"} is preferred over {@code ".ts"}).  When no
     * supplied suffix matches the final segment — or none are supplied — the
     * final dot-extension of that segment is stripped as a fallback, preserving
     * the legacy "strip last extension only" behaviour.  Suffixes that contain
     * dots (e.g. {@code ".test.ts"}, {@code "_test.py"}) match naturally.
     * </p>
     *
     * @param root            the scan root
     * @param file            the source file (expected to reside beneath {@code root})
     * @param suffixesToStrip candidate file-name suffixes to remove; may be empty
     * @return dot-separated stem; never {@code null}
     */
    public static String buildFileStem(Path root, Path file, String... suffixesToStrip) {
        Path rel = root.toAbsolutePath().normalize()
                .relativize(file.toAbsolutePath().normalize());
        int count = rel.getNameCount();
        StringBuilder sb = new StringBuilder(rel.toString().length());
        for (int i = 0; i < count; i++) {
            String segment = rel.getName(i).toString();
            if (i == count - 1) {
                segment = stripSuffix(segment, suffixesToStrip);
            }
            if (sb.length() > 0) {
                sb.append('.');
            }
            sb.append(segment);
        }
        return sb.toString();
    }

    /**
     * Strips the longest matching suffix from {@code segment}, falling back to
     * removing the final dot-extension when no supplied suffix matches.
     */
    private static String stripSuffix(String segment, String... suffixesToStrip) {
        String best = null;
        for (String suffix : suffixesToStrip) {
            if (suffix != null && !suffix.isEmpty() && segment.endsWith(suffix)
                    && (best == null || suffix.length() > best.length())) {
                best = suffix;
            }
        }
        if (best != null) {
            return segment.substring(0, segment.length() - best.length());
        }
        int lastDot = segment.lastIndexOf('.');
        return lastDot > 0 ? segment.substring(0, lastDot) : segment;
    }
}