UseStringIsEmptyRule.java

/*
 * Copyright (c) 2011-2024 Qulice.com
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the Qulice.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
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.rule.AbstractInefficientZeroCheck;
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;
import net.sourceforge.pmd.util.StringUtil;

/**
 * Rule to prohibit use of String.length() when checking for empty string.
 * String.isEmpty() should be used instead.
 * @since 0.18
 */
public final class UseStringIsEmptyRule extends AbstractInefficientZeroCheck {

    @Override
    public boolean appliesToClassName(final String name) {
        return 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)
                );
            }
        }
    }
}