UseStringIsEmptyRule.java

/*
 * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
 * SPDX-License-Identifier: MIT
 */
package com.qulice.pmd.rules;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTReferenceType;
import net.sourceforge.pmd.lang.java.ast.ASTResultType;
import net.sourceforge.pmd.lang.java.ast.ASTType;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.JavaNode;
import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence;
import net.sourceforge.pmd.lang.java.symboltable.MethodNameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;

/**
 * Rule to prohibit use of String.length() when checking for empty string.
 * String.isEmpty() should be used instead.
 * @since 0.18
 */
@SuppressWarnings("deprecation")
public final class UseStringIsEmptyRule
    extends net.sourceforge.pmd.lang.java.rule.AbstractInefficientZeroCheck {

    @Override
    public boolean appliesToClassName(final String name) {
        return net.sourceforge.pmd.util.StringUtil.isSame(
            name, "String", true, true, true
        );
    }

    @Override
    public Map<String, List<String>> getComparisonTargets() {
        final Map<String, List<String>> rules = new HashMap<>();
        rules.put("<", Arrays.asList("1"));
        rules.put(">", Arrays.asList("0"));
        rules.put("==", Arrays.asList("0"));
        rules.put("!=", Arrays.asList("0"));
        rules.put(">=", Arrays.asList("0", "1"));
        rules.put("<=", Arrays.asList("0"));
        return rules;
    }

    @Override
    public boolean isTargetMethod(final JavaNameOccurrence occ) {
        final NameOccurrence name = occ.getNameForWhichThisIsAQualifier();
        return name != null && "length".equals(name.getImage());
    }

    @Override
    public Object visit(
        final ASTVariableDeclaratorId variable, final Object data
    ) {
        final Node node = variable.getTypeNameNode();
        if (node instanceof ASTReferenceType) {
            final Class<?> clazz = variable.getType();
            final String type = variable.getNameDeclaration().getTypeImage();
            if (clazz != null && !clazz.isArray()
                && this.appliesToClassName(type)
            ) {
                final List<NameOccurrence> declarations = variable.getUsages();
                this.checkDeclarations(declarations, data);
            }
        }
        variable.childrenAccept(this, data);
        return data;
    }

    @Override
    public Object visit(
        final ASTMethodDeclaration declaration, final Object data
    ) {
        final ASTResultType result = declaration.getResultType();
        if (!result.isVoid()) {
            final ASTType node = (ASTType) result.jjtGetChild(0);
            final Class<?> clazz = node.getType();
            final String type = node.getTypeImage();
            if (clazz != null && !clazz.isArray()
                && this.appliesToClassName(type)
            ) {
                final Scope scope = declaration.getScope().getParent();
                final MethodNameDeclaration method = new MethodNameDeclaration(
                    declaration.getMethodDeclarator()
                );
                final List<NameOccurrence> declarations = scope
                    .getDeclarations(MethodNameDeclaration.class)
                    .get(method);
                this.checkDeclarations(declarations, data);
            }
        }
        declaration.childrenAccept(this, data);
        return data;
    }

    /**
     * Checks all uses of a variable or method with a String type.
     * @param occurrences Variable or method occurrences.
     * @param data Rule context.
     */
    private void checkDeclarations(
        final Iterable<NameOccurrence> occurrences, final Object data
    ) {
        for (final NameOccurrence occurrence : occurrences) {
            final JavaNameOccurrence jocc = (JavaNameOccurrence) occurrence;
            if (this.isTargetMethod(jocc)) {
                final JavaNode location = jocc.getLocation();
                final Node expr = location.getFirstParentOfType(
                    ASTExpression.class
                );
                this.checkNodeAndReport(
                    data, occurrence.getLocation(), expr.jjtGetChild(0)
                );
            }
        }
    }
}