Search in sources :

Example 36 with PhpType

use of com.jetbrains.php.lang.psi.resolve.types.PhpType in project phpinspectionsea by kalessil.

the class OnlyWritesOnParameterInspector method buildVisitor.

@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    return new BasePhpElementVisitor() {

        @Override
        public void visitPhpMethod(@NotNull Method method) {
            if (!method.isAbstract()) {
                this.visitPhpFunction(method);
            }
        }

        @Override
        public void visitPhpFunction(@NotNull Function function) {
            Arrays.stream(function.getParameters()).filter(parameter -> !parameter.getName().isEmpty() && !parameter.isPassByRef()).filter(parameter -> {
                final PhpType declaredType = OpenapiResolveUtil.resolveDeclaredType(parameter).filterUnknown().filterNull();
                final boolean isObject = !declaredType.isEmpty() && declaredType.getTypes().stream().anyMatch(t -> {
                    final String type = Types.getType(t);
                    return type.equals(Types.strObject) || type.startsWith("\\");
                });
                return !isObject;
            }).forEach(parameter -> this.analyzeAndReturnUsagesCount(parameter.getName(), function));
            final List<Variable> variables = ExpressionSemanticUtil.getUseListVariables(function);
            if (variables != null) {
                this.checkUseVariables(variables, function);
                variables.clear();
            }
        }

        @Override
        public void visitPhpAssignmentExpression(@NotNull AssignmentExpression assignment) {
            /* because this hook fired e.g. for `.=` assignments (a BC break by PhpStorm) */
            if (OpenapiTypesUtil.isAssignment(assignment)) {
                final PsiElement variable = assignment.getVariable();
                if (variable instanceof Variable) {
                    /* false-positives: predefined and global variables */
                    final String variableName = ((Variable) variable).getName();
                    if (variableName.isEmpty() || ExpressionCostEstimateUtil.predefinedVars.contains(variableName)) {
                        return;
                    }
                    /* filter target contexts: we are supporting only certain of them */
                    final PsiElement parent = assignment.getParent();
                    final boolean isTargetContext = parent instanceof ParenthesizedExpression || parent instanceof ArrayIndex || (parent instanceof BinaryExpression && OpenapiTypesUtil.tsCOMPARE_EQUALITY_OPS.contains(((BinaryExpression) parent).getOperationType())) || OpenapiTypesUtil.isStatementImpl(parent) || OpenapiTypesUtil.isAssignment(parent);
                    if (isTargetContext) {
                        final Function scope = ExpressionSemanticUtil.getScope(assignment);
                        if (scope != null && Arrays.stream(scope.getParameters()).noneMatch(p -> p.getName().equals(variableName))) {
                            final List<Variable> uses = ExpressionSemanticUtil.getUseListVariables(scope);
                            final boolean isUseVariable = uses != null && uses.stream().anyMatch(u -> u.getName().equals(variableName));
                            if (!isUseVariable) {
                                this.analyzeAndReturnUsagesCount(variableName, scope);
                            }
                        }
                    }
                }
            }
        }

        private void checkUseVariables(@NotNull List<Variable> variables, @NotNull Function function) {
            for (final Variable variable : variables) {
                final String parameterName = variable.getName();
                if (!parameterName.isEmpty()) {
                    PsiElement previous = variable.getPrevSibling();
                    if (previous instanceof PsiWhiteSpace) {
                        previous = previous.getPrevSibling();
                    }
                    if (OpenapiTypesUtil.is(previous, PhpTokenTypes.opBIT_AND)) {
                        if (OpenapiControlFlowUtil.getFollowingVariableAccessInstructions(function.getControlFlow().getEntryPoint(), parameterName).isEmpty()) {
                            holder.registerProblem(variable, MessagesPresentationUtil.prefixWithEa(messageUnused), ProblemHighlightType.LIKE_UNUSED_SYMBOL);
                        }
                    } else if (this.analyzeAndReturnUsagesCount(parameterName, function) == 0) {
                        holder.registerProblem(variable, MessagesPresentationUtil.prefixWithEa(messageUnused), ProblemHighlightType.LIKE_UNUSED_SYMBOL);
                    }
                }
            }
        }

        private int analyzeAndReturnUsagesCount(@NotNull String parameterName, @NotNull Function function) {
            final List<PhpAccessVariableInstruction> usages = OpenapiControlFlowUtil.getFollowingVariableAccessInstructions(function.getControlFlow().getEntryPoint(), parameterName);
            if (usages.isEmpty()) {
                return 0;
            }
            final List<PsiElement> targetExpressions = new ArrayList<>();
            boolean isReference = false;
            int intCountReadAccesses = 0;
            int intCountWriteAccesses = 0;
            for (final PhpAccessVariableInstruction instruction : usages) {
                final PsiElement variable = instruction.getAnchor();
                final PsiElement parent = variable.getParent();
                if (parent instanceof ArrayAccessExpression) {
                    /* find out which expression is holder */
                    PsiElement objLastSemanticExpression = variable;
                    PsiElement objTopSemanticExpression = objLastSemanticExpression.getParent();
                    /* TODO: iterator for array access expression */
                    while (objTopSemanticExpression instanceof ArrayAccessExpression) {
                        objLastSemanticExpression = objTopSemanticExpression;
                        objTopSemanticExpression = objTopSemanticExpression.getParent();
                    }
                    /* estimate operation type */
                    if (objTopSemanticExpression instanceof AssignmentExpression && ((AssignmentExpression) objTopSemanticExpression).getVariable() == objLastSemanticExpression) {
                        intCountWriteAccesses++;
                        if (isReference) {
                            /* when modifying the reference it's link READ and linked WRITE semantics */
                            intCountReadAccesses++;
                        } else {
                            /* when modifying non non-reference, register as write only access for reporting */
                            targetExpressions.add(objLastSemanticExpression);
                        }
                        continue;
                    }
                    if (objTopSemanticExpression instanceof UnaryExpression) {
                        final PsiElement operation = ((UnaryExpression) objTopSemanticExpression).getOperation();
                        if (OpenapiTypesUtil.is(operation, PhpTokenTypes.opINCREMENT) || OpenapiTypesUtil.is(operation, PhpTokenTypes.opDECREMENT)) {
                            targetExpressions.add(objLastSemanticExpression);
                            ++intCountWriteAccesses;
                            continue;
                        }
                    }
                    intCountReadAccesses++;
                    continue;
                }
                /* ++/-- operations */
                if (parent instanceof UnaryExpression) {
                    final PsiElement operation = ((UnaryExpression) parent).getOperation();
                    if (OpenapiTypesUtil.is(operation, PhpTokenTypes.opINCREMENT) || OpenapiTypesUtil.is(operation, PhpTokenTypes.opDECREMENT)) {
                        ++intCountWriteAccesses;
                        if (isReference) {
                            /* when modifying the reference it's link READ and linked WRITE semantics */
                            ++intCountReadAccesses;
                        } else {
                            /* when modifying non-reference, register as write only access for reporting */
                            targetExpressions.add(parent);
                        }
                    }
                    if (!OpenapiTypesUtil.isStatementImpl(parent.getParent())) {
                        ++intCountReadAccesses;
                    }
                    continue;
                }
                if (parent instanceof SelfAssignmentExpression) {
                    final SelfAssignmentExpression selfAssignment = (SelfAssignmentExpression) parent;
                    final PsiElement sameVariableCandidate = selfAssignment.getVariable();
                    if (sameVariableCandidate instanceof Variable) {
                        final Variable candidate = (Variable) sameVariableCandidate;
                        if (candidate.getName().equals(parameterName)) {
                            ++intCountWriteAccesses;
                            if (isReference) {
                                /* when modifying the reference it's link READ and linked WRITE semantics */
                                ++intCountReadAccesses;
                            } else {
                                /* when modifying non-reference, register as write only access for reporting */
                                targetExpressions.add(variable);
                            }
                            if (!OpenapiTypesUtil.isStatementImpl(parent.getParent())) {
                                ++intCountReadAccesses;
                            }
                            continue;
                        }
                    }
                }
                /* if variable assigned with reference, we need to preserve this information for correct checks */
                if (parent instanceof AssignmentExpression) {
                    /* ensure variable with the same name being written */
                    final AssignmentExpression referenceAssignmentCandidate = (AssignmentExpression) parent;
                    /* check if the target used as a container */
                    final PsiElement assignmentVariableCandidate = referenceAssignmentCandidate.getVariable();
                    if (assignmentVariableCandidate instanceof Variable) {
                        final Variable candidate = (Variable) assignmentVariableCandidate;
                        if (candidate.getName().equals(parameterName)) {
                            ++intCountWriteAccesses;
                            if (isReference) {
                                /* when modifying the reference it's link READ and linked WRITE semantics */
                                ++intCountReadAccesses;
                            }
                            /* now ensure operation is assignment of reference */
                            if (OpenapiTypesUtil.isAssignmentByReference(referenceAssignmentCandidate)) {
                                isReference = true;
                            }
                            /* false-negative: inline assignment result has been used */
                            if (usages.size() == 2 && usages.get(0).getAnchor() == usages.get(1).getAnchor()) {
                                holder.registerProblem(assignmentVariableCandidate, MessagesPresentationUtil.prefixWithEa(messageUnused), ProblemHighlightType.LIKE_UNUSED_SYMBOL);
                                return 1;
                            }
                            continue;
                        }
                    }
                    /* check if the target used as a value */
                    final PsiElement assignmentValueCandidate = referenceAssignmentCandidate.getValue();
                    if (assignmentValueCandidate instanceof Variable) {
                        final Variable candidate = (Variable) assignmentValueCandidate;
                        if (candidate.getName().equals(parameterName)) {
                            ++intCountReadAccesses;
                            continue;
                        }
                    }
                }
                /* local variables access wrongly reported write in some cases, so rely on custom checks */
                if (parent instanceof ParameterList || parent instanceof PhpUseList || parent instanceof PhpUnset || parent instanceof PhpEmpty || parent instanceof PhpIsset || parent instanceof ForeachStatement) {
                    intCountReadAccesses++;
                    continue;
                }
                /* ok variable usage works well with openapi */
                final PhpAccessInstruction.Access instructionAccess = instruction.getAccess();
                if (instructionAccess.isWrite()) {
                    targetExpressions.add(variable);
                    ++intCountWriteAccesses;
                } else if (instructionAccess.isRead()) {
                    ++intCountReadAccesses;
                }
            }
            if (intCountReadAccesses == 0 && intCountWriteAccesses > 0 && !this.isAnySuppressed(targetExpressions)) {
                final boolean report = IGNORE_INCLUDES || !this.hasIncludes(function);
                if (report) {
                    for (final PsiElement targetExpression : new HashSet<>(targetExpressions)) {
                        holder.registerProblem(targetExpression, MessagesPresentationUtil.prefixWithEa(messageOnlyWrites), ProblemHighlightType.LIKE_UNUSED_SYMBOL);
                    }
                }
            }
            targetExpressions.clear();
            return usages.size();
        }

        private boolean isAnySuppressed(@NotNull List<PsiElement> expressions) {
            for (final PsiElement one : expressions) {
                final PsiElement parent = one.getParent();
                if (parent instanceof AssignmentExpression) {
                    final PsiElement grandParent = parent.getParent();
                    if (OpenapiTypesUtil.isStatementImpl(grandParent)) {
                        final PsiElement previous = ((PhpPsiElement) grandParent).getPrevPsiSibling();
                        if (previous instanceof PhpDocComment) {
                            final String candidate = previous.getText();
                            if (candidate.contains("@noinspection") && candidate.contains(getShortName())) {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }

        private boolean hasIncludes(@NotNull Function function) {
            final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(function);
            if (body != null) {
                return PsiTreeUtil.findChildOfType(body, Include.class) != null;
            }
            return false;
        }
    };
}
Also used : PhpAccessInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessInstruction) PhpDocComment(com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment) Arrays(java.util.Arrays) BasePhpInspection(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection) com.jetbrains.php.lang.psi.elements(com.jetbrains.php.lang.psi.elements) PhpTokenTypes(com.jetbrains.php.lang.lexer.PhpTokenTypes) ExpressionCostEstimateUtil(com.kalessil.phpStorm.phpInspectionsEA.inspectors.ifs.utils.ExpressionCostEstimateUtil) OptionsComponent(com.kalessil.phpStorm.phpInspectionsEA.options.OptionsComponent) ArrayList(java.util.ArrayList) HashSet(java.util.HashSet) PsiTreeUtil(com.intellij.psi.util.PsiTreeUtil) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) com.kalessil.phpStorm.phpInspectionsEA.utils(com.kalessil.phpStorm.phpInspectionsEA.utils) List(java.util.List) PsiElement(com.intellij.psi.PsiElement) PsiWhiteSpace(com.intellij.psi.PsiWhiteSpace) ProblemHighlightType(com.intellij.codeInspection.ProblemHighlightType) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) NotNull(org.jetbrains.annotations.NotNull) PsiElementVisitor(com.intellij.psi.PsiElementVisitor) ProblemsHolder(com.intellij.codeInspection.ProblemsHolder) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) javax.swing(javax.swing) ArrayList(java.util.ArrayList) NotNull(org.jetbrains.annotations.NotNull) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) ArrayList(java.util.ArrayList) List(java.util.List) PhpAccessInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessInstruction) PsiElement(com.intellij.psi.PsiElement) HashSet(java.util.HashSet) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpDocComment(com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment) PsiWhiteSpace(com.intellij.psi.PsiWhiteSpace) NotNull(org.jetbrains.annotations.NotNull)

Example 37 with PhpType

use of com.jetbrains.php.lang.psi.resolve.types.PhpType in project phpinspectionsea by kalessil.

the class ChainedCallsStrategy method apply.

private static void apply(@NotNull MethodReference reference, @NotNull Map<MethodReference, String> nullTestedReferences, @NotNull ProblemsHolder holder) {
    final PsiElement operator = OpenapiPsiSearchUtil.findResolutionOperator(reference);
    if (OpenapiTypesUtil.is(operator, PhpTokenTypes.ARROW) && !OpenapiTypesUtil.isNullSafeMemberReferenceOperator(operator)) {
        final PsiElement base = reference.getFirstPsiChild();
        if (base instanceof FunctionReference) {
            final FunctionReference baseCall = (FunctionReference) base;
            final PhpType returnType = OpenapiResolveUtil.resolveType(baseCall, holder.getProject());
            if (returnType != null) {
                final String methodName = baseCall.getName();
                for (final String resolvedType : returnType.filterUnknown().getTypes()) {
                    final String type = Types.getType(resolvedType);
                    if (type.equals(Types.strNull) || type.equals(Types.strVoid)) {
                        boolean isNullTested = false;
                        for (final Map.Entry<MethodReference, String> knownReference : nullTestedReferences.entrySet()) {
                            final String nullTestedMethodName = knownReference.getValue();
                            if (nullTestedMethodName != null && nullTestedMethodName.equals(methodName) && OpenapiEquivalenceUtil.areEqual(knownReference.getKey(), baseCall)) {
                                isNullTested = true;
                                break;
                            }
                        }
                        if (!isNullTested) {
                            holder.registerProblem(operator, MessagesPresentationUtil.prefixWithEa(message));
                            break;
                        }
                    }
                }
            }
        }
        /* collect null-tested references: only after main inspection! */
        final PsiElement parent = reference.getParent();
        if (parent instanceof BinaryExpression) {
            final BinaryExpression parentExpression = (BinaryExpression) parent;
            final IElementType operation = parentExpression.getOperationType();
            if (OpenapiTypesUtil.tsCOMPARE_EQUALITY_OPS.contains(operation)) {
                final PsiElement secondOperand = OpenapiElementsUtil.getSecondOperand(parentExpression, reference);
                if (PhpLanguageUtil.isNull(secondOperand)) {
                    nullTestedReferences.put(reference, reference.getName());
                }
            } else if (operation == PhpTokenTypes.kwINSTANCEOF || PhpTokenTypes.tsSHORT_CIRCUIT_AND_OPS.contains(operation)) {
                nullTestedReferences.put(reference, reference.getName());
            }
        } else if (ExpressionSemanticUtil.isUsedAsLogicalOperand(reference)) {
            nullTestedReferences.put(reference, reference.getName());
        }
    }
}
Also used : IElementType(com.intellij.psi.tree.IElementType) BinaryExpression(com.jetbrains.php.lang.psi.elements.BinaryExpression) FunctionReference(com.jetbrains.php.lang.psi.elements.FunctionReference) MethodReference(com.jetbrains.php.lang.psi.elements.MethodReference) HashMap(java.util.HashMap) Map(java.util.Map) PsiElement(com.intellij.psi.PsiElement) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType)

Example 38 with PhpType

use of com.jetbrains.php.lang.psi.resolve.types.PhpType in project phpinspectionsea by kalessil.

the class UnnecessaryAssertionInspector method buildVisitor.

@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    return new BasePhpElementVisitor() {

        @Override
        public void visitPhpMethodReference(@NotNull MethodReference reference) {
            final String methodName = reference.getName();
            if (methodName != null) {
                if (methodName.startsWith("assert")) {
                    this.analyzeTypeHintCase(reference, methodName);
                } else {
                    this.analyzeMockingAsserts(reference, methodName);
                }
            }
        }

        private void analyzeMockingAsserts(@NotNull MethodReference reference, @NotNull String methodName) {
            if (methodName.equals("expects")) {
                final PsiElement[] arguments = reference.getParameters();
                if (arguments.length == 1 && arguments[0] instanceof MethodReference) {
                    final MethodReference innerReference = (MethodReference) arguments[0];
                    final String innerMethodName = innerReference.getName();
                    if (innerMethodName != null && innerMethodName.equals("any")) {
                        holder.registerProblem(innerReference, MessagesPresentationUtil.prefixWithEa(messageExpectsAny), new RemoveExpectsAssertionFixer(holder.getProject(), reference, reference.getFirstChild()));
                    }
                }
            }
        }

        private void analyzeTypeHintCase(@NotNull MethodReference reference, @NotNull String methodName) {
            final Project project = holder.getProject();
            if (PhpLanguageLevel.get(project).atLeast(PhpLanguageLevel.PHP700) && targetPositions.containsKey(methodName)) {
                final int position = targetPositions.get(methodName);
                final PsiElement[] arguments = reference.getParameters();
                if (arguments.length >= position + 1) {
                    final Set<PsiElement> values = PossibleValuesDiscoveryUtil.discover(arguments[position]);
                    if (values.size() == 1) {
                        final PsiElement candidate = values.iterator().next();
                        if (candidate instanceof FunctionReference) {
                            final FunctionReference call = (FunctionReference) candidate;
                            final PsiElement function = OpenapiResolveUtil.resolveReference(call);
                            if (function instanceof Function) {
                                final PsiElement returnType = OpenapiElementsUtil.getReturnType((Function) function);
                                if (returnType != null) {
                                    final PhpType resolved = OpenapiResolveUtil.resolveType(call, project);
                                    if (resolved != null && resolved.size() == 1 && !resolved.hasUnknown()) {
                                        /* find out what is expected */
                                        String expectedType = targetType.get(methodName);
                                        if (methodName.equals("assertInstanceOf")) {
                                            if (arguments[0] instanceof ClassConstantReference) {
                                                final ClassConstantReference expectation = (ClassConstantReference) arguments[0];
                                                final PsiElement base = expectation.getClassReference();
                                                if (base instanceof ClassReference) {
                                                    final PsiElement clazz = OpenapiResolveUtil.resolveReference((ClassReference) base);
                                                    if (clazz instanceof PhpClass) {
                                                        expectedType = ((PhpClass) clazz).getFQN();
                                                    }
                                                }
                                            }
                                        }
                                        /* match arguments types */
                                        final String expected = expectedType;
                                        if (expected == null) {
                                            holder.registerProblem(reference, MessagesPresentationUtil.prefixWithEa(messageReturnType));
                                        } else if (resolved.getTypes().stream().anyMatch(t -> Types.getType(t).equals(expected))) {
                                            holder.registerProblem(reference, MessagesPresentationUtil.prefixWithEa(messageReturnType));
                                        }
                                    }
                                }
                            }
                        }
                    }
                    values.clear();
                }
            }
        }
    };
}
Also used : BasePhpInspection(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection) PhpLanguageLevel(com.kalessil.phpStorm.phpInspectionsEA.openApi.PhpLanguageLevel) com.jetbrains.php.lang.psi.elements(com.jetbrains.php.lang.psi.elements) Set(java.util.Set) HashMap(java.util.HashMap) SmartPointerManager(com.intellij.psi.SmartPointerManager) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) com.kalessil.phpStorm.phpInspectionsEA.utils(com.kalessil.phpStorm.phpInspectionsEA.utils) ProblemDescriptor(com.intellij.codeInspection.ProblemDescriptor) SmartPsiElementPointer(com.intellij.psi.SmartPsiElementPointer) Map(java.util.Map) PsiElement(com.intellij.psi.PsiElement) Project(com.intellij.openapi.project.Project) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) NotNull(org.jetbrains.annotations.NotNull) LocalQuickFix(com.intellij.codeInspection.LocalQuickFix) PsiElementVisitor(com.intellij.psi.PsiElementVisitor) ProblemsHolder(com.intellij.codeInspection.ProblemsHolder) NotNull(org.jetbrains.annotations.NotNull) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) Project(com.intellij.openapi.project.Project) PsiElement(com.intellij.psi.PsiElement) NotNull(org.jetbrains.annotations.NotNull)

Example 39 with PhpType

use of com.jetbrains.php.lang.psi.resolve.types.PhpType in project phpinspectionsea by kalessil.

the class ReturnTypeCanBeDeclaredInspector method buildVisitor.

@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    return new BasePhpElementVisitor() {

        /* TODO: support functions - see https://github.com/kalessil/phpinspectionsea/pull/320 */
        @Override
        public void visitPhpMethod(@NotNull Method method) {
            final PhpLanguageLevel php = PhpLanguageLevel.get(holder.getProject());
            if (php.atLeast(PhpLanguageLevel.PHP700) && !magicMethods.contains(method.getName())) {
                final boolean isTarget = OpenapiElementsUtil.getReturnType(method) == null;
                if (isTarget) {
                    final PsiElement methodNameNode = NamedElementUtil.getNameIdentifier(method);
                    if (methodNameNode != null) {
                        final boolean supportNullableTypes = php.atLeast(PhpLanguageLevel.PHP710);
                        if (method.isAbstract()) {
                            this.handleAbstractMethod(method, methodNameNode, supportNullableTypes);
                        } else {
                            this.handleMethod(method, methodNameNode, supportNullableTypes);
                        }
                    }
                }
            }
        }

        private void handleAbstractMethod(@NotNull Method method, @NotNull PsiElement target, boolean supportNullableTypes) {
            final PhpDocComment docBlock = method.getDocComment();
            if (docBlock != null && docBlock.getReturnTag() != null) {
                this.handleMethod(method, target, supportNullableTypes);
            }
        }

        private void handleMethod(@NotNull Method method, @NotNull PsiElement target, boolean supportNullableTypes) {
            /* suggest nothing when the type is only partially resolved */
            final PhpType resolvedReturnType = OpenapiResolveUtil.resolveType(method, holder.getProject());
            if (resolvedReturnType == null) {
                return;
            } else if (resolvedReturnType.hasUnknown()) {
                /* adding class interface leading to promise-type for interface method */
                boolean isInfluencedByInterface = resolvedReturnType.size() == 2 && resolvedReturnType.filterUnknown().size() == 1;
                if (!isInfluencedByInterface) {
                    return;
                }
            }
            /* ignore DocBlock, resolve and normalize types instead (DocBlock is involved, but nevertheless) */
            final Set<String> normalizedTypes = resolvedReturnType.filterUnknown().getTypes().stream().map(Types::getType).collect(Collectors.toSet());
            this.checkUnrecognizedGenerator(method, normalizedTypes);
            this.checkReturnStatements(method, normalizedTypes);
            final boolean isVoidAvailable = PhpLanguageLevel.get(holder.getProject()).atLeast(PhpLanguageLevel.PHP710);
            final int typesCount = normalizedTypes.size();
            /* case 1: offer using void */
            if (supportNullableTypes && typesCount == 0 && isVoidAvailable) {
                final PsiElement firstReturn = PsiTreeUtil.findChildOfType(method, PhpReturn.class);
                if (firstReturn == null || ExpressionSemanticUtil.getScope(firstReturn) != method) {
                    final LocalQuickFix fixer = this.isMethodOverridden(method) ? null : new DeclareReturnTypeFix(Types.strVoid);
                    final String message = messagePattern.replace("%t%", Types.strVoid).replace("%n%", fixer == null ? " (please use change signature intention to fix this)" : "");
                    holder.registerProblem(target, MessagesPresentationUtil.prefixWithEa(message), fixer);
                }
            }
            /* case 2: offer using type */
            if (1 == typesCount) {
                final String singleType = normalizedTypes.iterator().next();
                final String suggestedType = isVoidAvailable && voidTypes.contains(singleType) ? Types.strVoid : this.compactType(singleType, method);
                final boolean isLegitBasic = singleType.startsWith("\\") || returnTypes.contains(singleType) || suggestedType.equals("self") || suggestedType.equals("static");
                final boolean isLegitVoid = !isLegitBasic && supportNullableTypes && suggestedType.equals(Types.strVoid);
                if (isLegitBasic || isLegitVoid) {
                    /* false-positive: '@return static' which is gets resolved into current class since 2019.2 */
                    final PhpDocComment docBlock = method.getDocComment();
                    final PhpDocReturnTag tag = docBlock == null ? null : docBlock.getReturnTag();
                    final boolean isStatic = tag != null && Arrays.stream(tag.getChildren()).map(PsiElement::getText).filter(t -> !t.isEmpty()).allMatch(t -> t.equals("static"));
                    final boolean isLegitStatic = isStatic && PhpLanguageLevel.get(holder.getProject()).atLeast(PhpLanguageLevel.PHP800);
                    if (!isStatic || isLegitStatic) {
                        final LocalQuickFix fixer = this.isMethodOverridden(method) ? null : new DeclareReturnTypeFix(isLegitStatic ? "static" : suggestedType);
                        final String message = messagePattern.replace("%t%", isLegitStatic ? "static" : suggestedType).replace("%n%", fixer == null ? " (please use change signature intention to fix this)" : "");
                        holder.registerProblem(target, MessagesPresentationUtil.prefixWithEa(message), fixer);
                    }
                }
            }
            /* case 3: offer using nullable type */
            if (supportNullableTypes && 2 == typesCount && normalizedTypes.contains(Types.strNull)) {
                normalizedTypes.remove(Types.strNull);
                final String nullableType = normalizedTypes.iterator().next();
                final String suggestedType = isVoidAvailable && voidTypes.contains(nullableType) ? Types.strVoid : compactType(nullableType, method);
                final boolean isLegitNullable = nullableType.startsWith("\\") || returnTypes.contains(nullableType) || suggestedType.equals("self");
                final boolean isLegitVoid = !isLegitNullable && suggestedType.equals(Types.strVoid);
                if (isLegitNullable || isLegitVoid) {
                    final String typeHint = isLegitVoid ? suggestedType : '?' + suggestedType;
                    final LocalQuickFix fixer = this.isMethodOverridden(method) ? null : new DeclareReturnTypeFix(typeHint);
                    final String message = messagePattern.replace("%t%", typeHint).replace("%n%", fixer == null ? " (please use change signature intention to fix this)" : "");
                    holder.registerProblem(target, MessagesPresentationUtil.prefixWithEa(message), fixer);
                }
            }
        }

        /* use change signature intention promoter */
        private boolean isMethodOverridden(@NotNull Method method) {
            boolean result = false;
            final PhpClass clazz = method.getContainingClass();
            if (clazz != null && !clazz.isFinal() && !method.isFinal() && !method.getAccess().isPrivate()) {
                final String methodName = method.getName();
                result = InterfacesExtractUtil.getCrawlInheritanceTree(clazz, true).stream().anyMatch(c -> c != clazz && c.findOwnMethodByName(methodName) != null) || OpenapiResolveUtil.resolveChildClasses(clazz.getFQN(), PhpIndex.getInstance(holder.getProject())).stream().anyMatch(c -> c.findOwnMethodByName(methodName) != null);
            }
            return result;
        }

        @NotNull
        private String compactType(@NotNull String type, @NotNull Method method) {
            String result = null;
            if (type.startsWith("\\") || type.equals("static")) {
                /* Strategy 1: respect `@return self` */
                if (LOOKUP_PHPDOC_RETURN_DECLARATIONS) {
                    final PhpDocComment phpDoc = method.getDocComment();
                    final PhpDocReturnTag phpReturn = phpDoc == null ? null : phpDoc.getReturnTag();
                    if (phpReturn != null) {
                        final boolean hasSelfReference = PsiTreeUtil.findChildrenOfType(phpReturn, PhpDocType.class).stream().anyMatch(t -> {
                            final String text = t.getText();
                            return text.equals("self") || text.equals("$this");
                        });
                        if (hasSelfReference) {
                            result = "self";
                        }
                    }
                }
                /* be sure to send back static for avoiding false-positives */
                result = (result == null && type.equals("static")) ? type : result;
                /* Strategy 2: scan imports */
                if (result == null) {
                    PsiElement groupCandidate = method.getContainingClass();
                    groups: while (groupCandidate != null && !(groupCandidate instanceof PsiFile)) {
                        if (groupCandidate instanceof GroupStatement) {
                            final List<PhpUse> imports = new ArrayList<>();
                            /* scan for imports in current group statement */
                            for (final PsiElement child : groupCandidate.getChildren()) {
                                if (child instanceof PhpUseList) {
                                    Collections.addAll(imports, ((PhpUseList) child).getDeclarations());
                                }
                            }
                            /* iterate imports and search for targets */
                            for (final PhpUse imported : imports) {
                                final PhpReference useReference = imported.getTargetReference();
                                if (useReference instanceof ClassReference && type.equals(useReference.getFQN())) {
                                    final String useAlias = imported.getAliasName();
                                    result = useAlias == null ? useReference.getName() : useAlias;
                                    imports.clear();
                                    break groups;
                                }
                            }
                            imports.clear();
                        }
                        groupCandidate = groupCandidate.getParent();
                    }
                }
                /* Strategy 3: relative QN for classes in sub-namespace */
                if (result == null || result.isEmpty()) {
                    final PhpClass clazz = method.getContainingClass();
                    final String nameSpace = null == clazz ? null : clazz.getNamespaceName();
                    if (nameSpace != null && nameSpace.length() > 1 && type.startsWith(nameSpace)) {
                        result = type.replace(nameSpace, "");
                    }
                }
            }
            return result == null ? type : result;
        }

        private void checkUnrecognizedGenerator(@NotNull Method method, @NotNull Set<String> types) {
            if (!types.contains("\\Generator")) {
                final PhpYield yield = PsiTreeUtil.findChildOfType(method, PhpYield.class);
                if (yield != null && ExpressionSemanticUtil.getScope(yield) == method) {
                    types.add("\\Generator");
                    if (PsiTreeUtil.findChildOfType(method, PhpReturn.class) == null) {
                        types.remove(Types.strNull);
                    }
                }
            }
        }

        private void checkReturnStatements(@NotNull Method method, @NotNull Set<String> types) {
            if (!types.isEmpty() && !method.isAbstract()) {
                /* non-implicit null return: omitted last return statement */
                if (!types.contains(Types.strNull) && !types.contains(Types.strVoid)) {
                    final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(method);
                    final PsiElement last = body == null ? null : ExpressionSemanticUtil.getLastStatement(body);
                    if (!(last instanceof PhpReturn) && !OpenapiTypesUtil.isThrowExpression(last)) {
                        types.add(Types.strNull);
                    }
                }
                /* buggy parameter type resolving: no type, but null as default value */
                if (types.size() == 1 && types.contains(Types.strNull)) {
                    final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(method);
                    if (body != null) {
                        final PhpReturn expression = PsiTreeUtil.findChildOfType(body, PhpReturn.class);
                        if (expression != null) {
                            final PsiElement value = ExpressionSemanticUtil.getReturnValue(expression);
                            if (value != null && !PhpLanguageUtil.isNull(value)) {
                                types.remove(Types.strNull);
                            }
                        }
                    }
                }
            }
        }
    };
}
Also used : PhpDocComment(com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment) java.util(java.util) com.jetbrains.php.lang.psi.elements(com.jetbrains.php.lang.psi.elements) PhpTokenTypes(com.jetbrains.php.lang.lexer.PhpTokenTypes) PhpDocType(com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocType) PhpIndex(com.jetbrains.php.PhpIndex) PsiTreeUtil(com.intellij.psi.util.PsiTreeUtil) PhpDocReturnTag(com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocReturnTag) ProblemDescriptor(com.intellij.codeInspection.ProblemDescriptor) PsiElement(com.intellij.psi.PsiElement) PsiWhiteSpace(com.intellij.psi.PsiWhiteSpace) Project(com.intellij.openapi.project.Project) PsiFile(com.intellij.psi.PsiFile) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) LocalQuickFix(com.intellij.codeInspection.LocalQuickFix) PsiElementVisitor(com.intellij.psi.PsiElementVisitor) ProblemsHolder(com.intellij.codeInspection.ProblemsHolder) PhpPsiElementFactory(com.jetbrains.php.lang.psi.PhpPsiElementFactory) BasePhpInspection(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection) PhpLanguageLevel(com.kalessil.phpStorm.phpInspectionsEA.openApi.PhpLanguageLevel) InterfacesExtractUtil(com.kalessil.phpStorm.phpInspectionsEA.utils.hierarhy.InterfacesExtractUtil) OptionsComponent(com.kalessil.phpStorm.phpInspectionsEA.options.OptionsComponent) Collectors(java.util.stream.Collectors) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) com.kalessil.phpStorm.phpInspectionsEA.utils(com.kalessil.phpStorm.phpInspectionsEA.utils) NotNull(org.jetbrains.annotations.NotNull) javax.swing(javax.swing) LocalQuickFix(com.intellij.codeInspection.LocalQuickFix) NotNull(org.jetbrains.annotations.NotNull) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) PhpDocReturnTag(com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocReturnTag) PsiFile(com.intellij.psi.PsiFile) PsiElement(com.intellij.psi.PsiElement) PhpLanguageLevel(com.kalessil.phpStorm.phpInspectionsEA.openApi.PhpLanguageLevel) PhpDocComment(com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment) NotNull(org.jetbrains.annotations.NotNull)

Example 40 with PhpType

use of com.jetbrains.php.lang.psi.resolve.types.PhpType in project phpinspectionsea by kalessil.

the class VariableFunctionsUsageInspector method buildVisitor.

@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    return new BasePhpElementVisitor() {

        @Override
        public void visitPhpFunctionCall(@NotNull FunctionReference reference) {
            final String functionName = reference.getName();
            if (functionName != null) {
                /* case: `call_user_func_array(..., array(...))` */
                if (arrayFunctionsMapping.containsKey(functionName)) {
                    final PsiElement[] arguments = reference.getParameters();
                    if (arguments.length == 2 && arguments[1] instanceof ArrayCreationExpression) {
                        final List<PsiElement> dispatched = this.extract((ArrayCreationExpression) arguments[1]);
                        if (!dispatched.isEmpty()) {
                            final boolean hasByReferences = dispatched.stream().anyMatch(argument -> this.argumentAsString(argument).startsWith("&"));
                            if (!hasByReferences) {
                                final String replacement = String.format("%s(%s, %s)", arrayFunctionsMapping.get(functionName), arguments[0].getText(), dispatched.stream().map(this::argumentAsString).collect(Collectors.joining(", ")));
                                holder.registerProblem(reference, String.format(MessagesPresentationUtil.prefixWithEa(patternInlineArgs), replacement), new InlineFix(replacement));
                            }
                            dispatched.clear();
                        }
                    }
                } else if (callerFunctions.contains(functionName)) {
                    final PsiElement[] arguments = reference.getParameters();
                    if (arguments.length > 0) {
                        /* extract callable */
                        final List<PsiElement> callable = new ArrayList<>();
                        if (arguments[0] instanceof ArrayCreationExpression) {
                            final List<PsiElement> extracted = this.extract((ArrayCreationExpression) arguments[0]);
                            if (!extracted.isEmpty()) {
                                /* false-positive: first part must not be a string - '<string>->...' is invalid code */
                                final PsiElement candidate = extracted.get(0);
                                if (candidate instanceof Variable) {
                                    final PhpType resolved = OpenapiResolveUtil.resolveType((PhpTypedElement) candidate, holder.getProject());
                                    final boolean skip = resolved == null || resolved.hasUnknown() || resolved.getTypes().stream().anyMatch(type -> Types.getType(type).equals(Types.strString));
                                    if (skip) {
                                        extracted.clear();
                                        return;
                                    }
                                }
                                /* regular behaviour */
                                callable.addAll(extracted);
                                extracted.clear();
                            }
                        } else {
                            /* false-positive: $func(...) is not working for arrays in PHP below 5.4 */
                            if (arguments[0] instanceof Variable && PhpLanguageLevel.get(holder.getProject()).below(PhpLanguageLevel.PHP540)) {
                                return;
                            }
                            /* regular behaviour */
                            callable.add(arguments[0]);
                        }
                        final PsiElement first = !callable.isEmpty() ? callable.get(0) : null;
                        final PsiElement second = callable.size() > 1 ? callable.get(1) : null;
                        final Couple<String> suggestedCallable = this.callable(first, second);
                        if (suggestedCallable.getFirst() != null) {
                            final String replacement;
                            if (suggestedCallable.getSecond() != null) {
                                final boolean useScopeResolution = (!(first instanceof Variable) || functionName.equals("forward_static_call"));
                                replacement = String.format("%s%s%s(%s)", suggestedCallable.getFirst(), useScopeResolution ? "::" : "->", suggestedCallable.getSecond(), Stream.of(Arrays.copyOfRange(arguments, 1, arguments.length)).map(this::argumentAsString).collect(Collectors.joining(", ")));
                            } else {
                                replacement = String.format("%s(%s)", suggestedCallable.getFirst(), Stream.of(Arrays.copyOfRange(arguments, 1, arguments.length)).map(this::argumentAsString).collect(Collectors.joining(", ")));
                            }
                            holder.registerProblem(reference, String.format(MessagesPresentationUtil.prefixWithEa(patternReplace), replacement), new ReplaceFix(replacement));
                        }
                        callable.clear();
                    }
                }
            }
        }

        @NotNull
        private Couple<String> callable(@Nullable PsiElement first, @Nullable PsiElement second) {
            final String[] callable = { null, null };
            /* check second part: in some cases it overrides the first one completely */
            if (second != null) {
                callable[1] = '{' + second.getText() + '}';
                if (second instanceof StringLiteralExpression) {
                    final StringLiteralExpression literal = (StringLiteralExpression) second;
                    if (literal.getFirstPsiChild() == null) {
                        final String content = PhpStringUtil.unescapeText(literal.getContents(), literal.isSingleQuote());
                        /* false-positives: relative invocation */
                        if (content.startsWith("parent::")) {
                            return Couple.of(null, null);
                        }
                        /* special case: the second overrides first one */
                        if (content.indexOf(':') != -1) {
                            return Couple.of(content, null);
                        }
                        /* regular behaviour cases */
                        callable[1] = content;
                    }
                } else if (second instanceof Variable) {
                    callable[1] = second.getText();
                }
            }
            /* the first part should be a variable or string literal without injections */
            if (first instanceof StringLiteralExpression) {
                final StringLiteralExpression literal = (StringLiteralExpression) first;
                if (literal.getFirstPsiChild() == null) {
                    callable[0] = PhpStringUtil.unescapeText(literal.getContents(), literal.isSingleQuote());
                }
            } else if (first instanceof Variable) {
                callable[0] = first.getText();
            }
            return Couple.of(callable[0], callable[1]);
        }

        @NotNull
        private List<PsiElement> extract(@NotNull ArrayCreationExpression container) {
            return Stream.of(container.getChildren()).map(child -> {
                if (child instanceof ArrayHashElement) {
                    return ((ArrayHashElement) child).getValue();
                }
                if (child instanceof PhpPsiElement) {
                    return ((PhpPsiElement) child).getFirstPsiChild();
                }
                return null;
            }).filter(Objects::nonNull).collect(Collectors.toList());
        }

        @NotNull
        private String argumentAsString(@NotNull PsiElement argument) {
            PsiElement previous = argument.getPrevSibling();
            if (previous instanceof PsiWhiteSpace) {
                previous = previous.getPrevSibling();
            }
            if (OpenapiTypesUtil.is(previous, PhpTokenTypes.opBIT_AND)) {
                return '&' + argument.getText();
            } else if (OpenapiTypesUtil.is(argument.getPrevSibling(), PhpTokenTypes.opVARIADIC)) {
                return "..." + argument.getText();
            }
            return argument.getText();
        }
    };
}
Also used : Couple(com.intellij.openapi.util.Couple) NotNull(org.jetbrains.annotations.NotNull) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) PsiElement(com.intellij.psi.PsiElement) Nullable(org.jetbrains.annotations.Nullable) PsiWhiteSpace(com.intellij.psi.PsiWhiteSpace) NotNull(org.jetbrains.annotations.NotNull)

Aggregations

PhpType (com.jetbrains.php.lang.psi.resolve.types.PhpType)56 PsiElement (com.intellij.psi.PsiElement)46 NotNull (org.jetbrains.annotations.NotNull)41 BasePhpElementVisitor (com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor)33 ProblemsHolder (com.intellij.codeInspection.ProblemsHolder)20 Project (com.intellij.openapi.project.Project)20 HashSet (java.util.HashSet)20 BasePhpInspection (com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection)16 Nullable (org.jetbrains.annotations.Nullable)16 com.jetbrains.php.lang.psi.elements (com.jetbrains.php.lang.psi.elements)15 PsiElementVisitor (com.intellij.psi.PsiElementVisitor)14 PhpIndex (com.jetbrains.php.PhpIndex)14 com.kalessil.phpStorm.phpInspectionsEA.utils (com.kalessil.phpStorm.phpInspectionsEA.utils)13 IElementType (com.intellij.psi.tree.IElementType)12 Set (java.util.Set)12 PhpTokenTypes (com.jetbrains.php.lang.lexer.PhpTokenTypes)10 PsiTreeUtil (com.intellij.psi.util.PsiTreeUtil)9 PhpTypedElement (com.jetbrains.php.lang.psi.elements.PhpTypedElement)9 InterfacesExtractUtil (com.kalessil.phpStorm.phpInspectionsEA.utils.hierarhy.InterfacesExtractUtil)8 Collectors (java.util.stream.Collectors)8