Search in sources :

Example 1 with PhpEntryPointInstruction

use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction in project phpinspectionsea by kalessil.

the class CallableParameterUseCaseInTypeContextInspection method buildVisitor.

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

        @Override
        public void visitPhpMethod(@NotNull Method method) {
            this.inspectUsages(method.getParameters(), method);
        }

        @Override
        public void visitPhpFunction(@NotNull Function function) {
            this.inspectUsages(function.getParameters(), function);
        }

        private void inspectUsages(@NotNull Parameter[] parameters, @NotNull PhpScopeHolder scopeHolder) {
            final Project project = holder.getProject();
            final PhpIndex index = PhpIndex.getInstance(project);
            final PhpEntryPointInstruction entryPoint = scopeHolder.getControlFlow().getEntryPoint();
            for (final Parameter parameter : parameters) {
                /* normalize parameter types, skip analysis when mixed or object appears */
                final Set<String> paramTypes = new HashSet<>();
                for (final String type : parameter.getType().global(project).filterUnknown().getTypes()) {
                    final String typeNormalized = Types.getType(type);
                    if (typeNormalized.equals(Types.strMixed) || typeNormalized.equals(Types.strObject)) {
                        paramTypes.clear();
                        break;
                    } else if (typeNormalized.equals(Types.strCallable)) {
                        paramTypes.add(Types.strArray);
                        paramTypes.add(Types.strString);
                        paramTypes.add("\\Closure");
                    } else if (typeNormalized.equals(Types.strIterable)) {
                        paramTypes.add(Types.strArray);
                        paramTypes.add("\\Traversable");
                    }
                    paramTypes.add(typeNormalized);
                }
                if (paramTypes.isEmpty()) {
                    continue;
                } else {
                    /* in some case PhpStorm is not recognizing default value as parameter type */
                    final PsiElement defaultValue = parameter.getDefaultValue();
                    if (defaultValue instanceof PhpTypedElement) {
                        final PhpType defaultType = OpenapiResolveUtil.resolveType((PhpTypedElement) defaultValue, project);
                        if (defaultType != null) {
                            defaultType.filterUnknown().getTypes().forEach(t -> paramTypes.add(Types.getType(t)));
                        }
                    }
                }
                /* false-positive: type is not resolved correctly, default null is taken */
                if (paramTypes.size() == 1 && paramTypes.contains(Types.strNull)) {
                    final PsiElement defaultValue = parameter.getDefaultValue();
                    if (defaultValue != null && PhpLanguageUtil.isNull(defaultValue)) {
                        continue;
                    }
                }
                /* now find instructions operating on the parameter and perform analysis */
                final String parameterName = parameter.getName();
                final PhpAccessVariableInstruction[] usages = PhpControlFlowUtil.getFollowingVariableAccessInstructions(entryPoint, parameterName, false);
                for (final PhpAccessVariableInstruction instruction : usages) {
                    final PsiElement parent = instruction.getAnchor().getParent();
                    final PsiElement callCandidate = null == parent ? null : parent.getParent();
                    /* Case 1: check if is_* functions being used according to definitions */
                    if (OpenapiTypesUtil.isFunctionReference(callCandidate)) {
                        final FunctionReference functionCall = (FunctionReference) callCandidate;
                        final String functionName = functionCall.getName();
                        if (functionName == null) {
                            continue;
                        }
                        /* we expect that aliases usage has been fixed already */
                        final boolean isTypeAnnounced;
                        switch(functionName) {
                            case "is_array":
                                isTypeAnnounced = paramTypes.contains(Types.strArray) || paramTypes.contains(Types.strIterable);
                                break;
                            case "is_string":
                                isTypeAnnounced = paramTypes.contains(Types.strString);
                                break;
                            case "is_bool":
                                isTypeAnnounced = paramTypes.contains(Types.strBoolean);
                                break;
                            case "is_int":
                                isTypeAnnounced = paramTypes.contains(Types.strInteger) || paramTypes.contains(Types.strNumber);
                                break;
                            case "is_float":
                                isTypeAnnounced = paramTypes.contains(Types.strFloat) || paramTypes.contains(Types.strNumber);
                                break;
                            case "is_resource":
                                isTypeAnnounced = paramTypes.contains(Types.strResource);
                                break;
                            case "is_numeric":
                                isTypeAnnounced = paramTypes.contains(Types.strNumber) || paramTypes.contains(Types.strString) || paramTypes.contains(Types.strFloat) || paramTypes.contains(Types.strInteger);
                                break;
                            case "is_callable":
                                isTypeAnnounced = paramTypes.contains(Types.strCallable) || paramTypes.contains(Types.strArray) || paramTypes.contains(Types.strString) || paramTypes.contains("\\Closure");
                                break;
                            case "is_object":
                                isTypeAnnounced = paramTypes.contains(Types.strObject) || paramTypes.stream().anyMatch(t -> (t.startsWith("\\") && !t.equals("\\Closure")) || classReferences.contains(t));
                                break;
                            case "is_a":
                                isTypeAnnounced = paramTypes.contains(Types.strObject) || paramTypes.contains(Types.strString) || paramTypes.stream().anyMatch(t -> (t.startsWith("\\") && !t.equals("\\Closure")) || classReferences.contains(t));
                                break;
                            default:
                                continue;
                        }
                        /* cases: call makes no sense, violation of defined types set */
                        if (!isTypeAnnounced) {
                            final PsiElement callParent = functionCall.getParent();
                            boolean isReversedCheck = false;
                            if (callParent instanceof UnaryExpression) {
                                final PsiElement operation = ((UnaryExpression) callParent).getOperation();
                                isReversedCheck = OpenapiTypesUtil.is(operation, PhpTokenTypes.opNOT);
                            }
                            holder.registerProblem(functionCall, isReversedCheck ? messageNoSense : messageViolationInCheck);
                        }
                        continue;
                    }
                    /* Case 2: assignments violating parameter definition */
                    if (OpenapiTypesUtil.isAssignment(parent)) {
                        final AssignmentExpression assignment = (AssignmentExpression) parent;
                        final PhpPsiElement variable = assignment.getVariable();
                        final PhpPsiElement value = assignment.getValue();
                        if (variable instanceof Variable && value instanceof PhpTypedElement) {
                            final String variableName = variable.getName();
                            if (variableName != null && variableName.equals(parameterName)) {
                                final PhpType resolvedType = OpenapiResolveUtil.resolveType((PhpTypedElement) value, project);
                                final Set<String> resolved = new HashSet<>();
                                if (resolvedType != null) {
                                    resolvedType.filterUnknown().getTypes().forEach(t -> resolved.add(Types.getType(t)));
                                }
                                if (resolved.size() >= 2) {
                                    /* false-positives: core functions returning string|false, string|null */
                                    if (resolved.contains(Types.strString)) {
                                        if (resolved.contains(Types.strBoolean)) {
                                            final boolean isFunctionCall = OpenapiTypesUtil.isFunctionReference(value);
                                            if (isFunctionCall) {
                                                resolved.remove(Types.strBoolean);
                                            }
                                        } else if (resolved.contains(Types.strNull)) {
                                            final boolean isFunctionCall = OpenapiTypesUtil.isFunctionReference(value);
                                            if (isFunctionCall) {
                                                resolved.remove(Types.strNull);
                                                /* preg_replace got better stub and brought lots of false-positives */
                                                if ("preg_replace".equals(value.getName())) {
                                                    resolved.remove(Types.strArray);
                                                }
                                            }
                                        }
                                    } else /* false-positives: nullable objects */
                                    if (resolved.contains(Types.strNull)) {
                                        final boolean isNullableObject = paramTypes.stream().anyMatch(t -> t.startsWith("\\") && !t.equals("\\Closure") || classReferences.contains(t));
                                        if (isNullableObject) {
                                            resolved.remove(Types.strNull);
                                        }
                                    }
                                }
                                resolved.remove(Types.strMixed);
                                for (String type : resolved) {
                                    /* translate static/self into FQNs */
                                    if (classReferences.contains(type)) {
                                        PsiElement valueExtract = value;
                                        /* ` = <whatever> ?? <method call>` support */
                                        if (valueExtract instanceof BinaryExpression) {
                                            final BinaryExpression binary = (BinaryExpression) valueExtract;
                                            if (binary.getOperationType() == PhpTokenTypes.opCOALESCE) {
                                                final PsiElement left = binary.getLeftOperand();
                                                if (left != null && OpeanapiEquivalenceUtil.areEqual(variable, left)) {
                                                    final PsiElement right = binary.getRightOperand();
                                                    if (right != null) {
                                                        valueExtract = right;
                                                    }
                                                }
                                            }
                                        }
                                        /* method call lookup */
                                        if (valueExtract instanceof MethodReference) {
                                            final PsiElement base = valueExtract.getFirstChild();
                                            if (base instanceof ClassReference) {
                                                final PsiElement resolvedClass = OpenapiResolveUtil.resolveReference((ClassReference) base);
                                                if (resolvedClass instanceof PhpClass) {
                                                    type = ((PhpClass) resolvedClass).getFQN();
                                                }
                                            } else if (base instanceof PhpTypedElement) {
                                                final PhpType clazzTypes = OpenapiResolveUtil.resolveType((PhpTypedElement) base, project);
                                                if (clazzTypes != null) {
                                                    final Set<String> filteredTypes = clazzTypes.filterUnknown().getTypes().stream().map(Types::getType).filter(t -> t.startsWith("\\")).collect(Collectors.toSet());
                                                    if (filteredTypes.size() == 1) {
                                                        type = filteredTypes.iterator().next();
                                                    }
                                                    filteredTypes.clear();
                                                }
                                            }
                                        }
                                    }
                                    final boolean isViolation = !this.isTypeCompatibleWith(type, paramTypes, index);
                                    if (isViolation) {
                                        final String message = patternViolationInAssignment.replace("%s%", type);
                                        holder.registerProblem(value, message);
                                        break;
                                    }
                                }
                                resolved.clear();
                            }
                        }
                    }
                }
                paramTypes.clear();
            }
        }

        private boolean isTypeCompatibleWith(@NotNull String type, @NotNull Collection<String> allowedTypes, @NotNull PhpIndex index) {
            /* first case: implicit match */
            if (allowedTypes.contains(type)) {
                return true;
            }
            /* second case: inherited classes/interfaces */
            final Set<String> possibleTypes = new HashSet<>();
            if (type.startsWith("\\")) {
                index.getAnyByFQN(type).forEach(clazz -> InterfacesExtractUtil.getCrawlInheritanceTree(clazz, true).forEach(c -> possibleTypes.add(c.getFQN())));
            }
            return !possibleTypes.isEmpty() && allowedTypes.stream().anyMatch(possibleTypes::contains);
        }
    };
}
Also used : PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) BasePhpInspection(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection) com.jetbrains.php.lang.psi.elements(com.jetbrains.php.lang.psi.elements) InterfacesExtractUtil(com.kalessil.phpStorm.phpInspectionsEA.utils.hierarhy.InterfacesExtractUtil) PhpTokenTypes(com.jetbrains.php.lang.lexer.PhpTokenTypes) Collection(java.util.Collection) Set(java.util.Set) PhpIndex(com.jetbrains.php.PhpIndex) PhpControlFlowUtil(com.jetbrains.php.codeInsight.controlFlow.PhpControlFlowUtil) Collectors(java.util.stream.Collectors) HashSet(java.util.HashSet) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) com.kalessil.phpStorm.phpInspectionsEA.utils(com.kalessil.phpStorm.phpInspectionsEA.utils) 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) PsiElementVisitor(com.intellij.psi.PsiElementVisitor) PhpScopeHolder(com.jetbrains.php.codeInsight.PhpScopeHolder) ProblemsHolder(com.intellij.codeInspection.ProblemsHolder) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) Set(java.util.Set) HashSet(java.util.HashSet) PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) NotNull(org.jetbrains.annotations.NotNull) PhpType(com.jetbrains.php.lang.psi.resolve.types.PhpType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) PhpScopeHolder(com.jetbrains.php.codeInsight.PhpScopeHolder) PsiElement(com.intellij.psi.PsiElement) HashSet(java.util.HashSet) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpIndex(com.jetbrains.php.PhpIndex) Project(com.intellij.openapi.project.Project) Collection(java.util.Collection) NotNull(org.jetbrains.annotations.NotNull)

Example 2 with PhpEntryPointInstruction

use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction in project phpinspectionsea by kalessil.

the class OneTimeUseVariablesInspector method buildVisitor.

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

        void checkOneTimeUse(@NotNull PhpPsiElement construct, @NotNull Variable argument) {
            final String variableName = argument.getName();
            final PsiElement previous = construct.getPrevPsiSibling();
            /* verify preceding expression (assignment needed) */
            if (null != previous && OpenapiTypesUtil.isAssignment(previous.getFirstChild())) {
                final AssignmentExpression assign = (AssignmentExpression) previous.getFirstChild();
                /* ensure variables are the same */
                final PhpPsiElement assignVariable = assign.getVariable();
                final PsiElement assignValue = ExpressionSemanticUtil.getExpressionTroughParenthesis(assign.getValue());
                if (null != assignValue && assignVariable instanceof Variable) {
                    final String assignVariableName = assignVariable.getName();
                    if (assignVariableName == null || !assignVariableName.equals(variableName)) {
                        return;
                    }
                    /* check if variable as a function/use(...) parameter by reference */
                    final Function function = ExpressionSemanticUtil.getScope(construct);
                    if (null != function) {
                        for (Parameter param : function.getParameters()) {
                            if (param.isPassByRef() && param.getName().equals(variableName)) {
                                return;
                            }
                        }
                        final List<Variable> useList = ExpressionSemanticUtil.getUseListVariables(function);
                        if (null != useList) {
                            for (Variable param : useList) {
                                if (!param.getName().equals(variableName)) {
                                    continue;
                                }
                                /* detect parameters by reference in use clause */
                                PsiElement ampersandCandidate = param.getPrevSibling();
                                if (ampersandCandidate instanceof PsiWhiteSpace) {
                                    ampersandCandidate = ampersandCandidate.getPrevSibling();
                                }
                                if (null != ampersandCandidate && ampersandCandidate.getText().equals("&")) {
                                    return;
                                }
                            }
                            useList.clear();
                        }
                    }
                    /* too long return/throw statements can be decoupled as a variable */
                    if (!ALLOW_LONG_STATEMENTS && assign.getText().length() > 80) {
                        return;
                    }
                    /* heavy part, find usage inside function/method to analyze multiple writes */
                    final PhpScopeHolder parentScope = ExpressionSemanticUtil.getScope(assign);
                    if (null != parentScope) {
                        final PhpEntryPointInstruction entryPoint = parentScope.getControlFlow().getEntryPoint();
                        final PhpAccessVariableInstruction[] usages = PhpControlFlowUtil.getFollowingVariableAccessInstructions(entryPoint, variableName, false);
                        int countWrites = 0;
                        int countReads = 0;
                        for (PhpAccessVariableInstruction oneCase : usages) {
                            final boolean isWrite = oneCase.getAccess().isWrite();
                            countWrites += isWrite ? 1 : 0;
                            countReads += isWrite ? 0 : 1;
                            if (countWrites > 1 || countReads > 1) {
                                return;
                            }
                        }
                    }
                    final String message = messagePattern.replace("%v%", variableName);
                    final TheLocalFix fixer = new TheLocalFix(assign.getParent(), argument, assignValue);
                    holder.registerProblem(assignVariable, message, fixer);
                }
            }
        }

        @Override
        public void visitPhpReturn(@NotNull PhpReturn returnStatement) {
            /* if function returning reference, do not inspect returns */
            final Function callable = ExpressionSemanticUtil.getScope(returnStatement);
            final PsiElement nameNode = NamedElementUtil.getNameIdentifier(callable);
            if (null != callable && null != nameNode) {
                /* is defined like returning reference */
                PsiElement referenceCandidate = nameNode.getPrevSibling();
                if (referenceCandidate instanceof PsiWhiteSpace) {
                    referenceCandidate = referenceCandidate.getPrevSibling();
                }
                if (OpenapiTypesUtil.is(referenceCandidate, PhpTokenTypes.opBIT_AND)) {
                    return;
                }
            }
            /* regular function, check one-time use variables */
            final PsiElement argument = ExpressionSemanticUtil.getExpressionTroughParenthesis(returnStatement.getArgument());
            if (argument instanceof PhpPsiElement) {
                final Variable variable = this.getVariable((PhpPsiElement) argument);
                if (null != variable) {
                    checkOneTimeUse(returnStatement, variable);
                }
            }
        }

        @Override
        public void visitPhpMultiassignmentExpression(@NotNull MultiassignmentExpression multiassignmentExpression) {
            final PsiElement firstChild = multiassignmentExpression.getFirstChild();
            final IElementType nodeType = null == firstChild ? null : firstChild.getNode().getElementType();
            if (null != nodeType && (PhpTokenTypes.kwLIST == nodeType || PhpTokenTypes.chLBRACKET == nodeType)) {
                final Variable variable = this.getVariable(multiassignmentExpression.getValue());
                final PsiElement parent = multiassignmentExpression.getParent();
                if (null != variable && OpenapiTypesUtil.isStatementImpl(parent)) {
                    checkOneTimeUse((PhpPsiElement) parent, variable);
                }
            }
        }

        /* TODO: once got bored, add foreach source pattern here =) I'm naive but nevertheless ^_^ */
        @Override
        public void visitPhpThrow(@NotNull PhpThrow throwStatement) {
            final PsiElement argument = ExpressionSemanticUtil.getExpressionTroughParenthesis(throwStatement.getArgument());
            if (argument instanceof PhpPsiElement) {
                final Variable variable = this.getVariable((PhpPsiElement) argument);
                if (null != variable) {
                    checkOneTimeUse(throwStatement, variable);
                }
            }
        }

        @Nullable
        private Variable getVariable(@Nullable PhpPsiElement expression) {
            if (null == expression) {
                return null;
            }
            if (expression instanceof Variable) {
                return (Variable) expression;
            }
            if (expression instanceof FieldReference) {
                final FieldReference propertyAccess = (FieldReference) expression;
                if (propertyAccess.getFirstChild() instanceof Variable) {
                    return (Variable) propertyAccess.getFirstChild();
                }
            }
            /* instanceof passes child classes as well, what isn't correct */
            if (OpenapiTypesUtil.isPhpExpressionImpl(expression)) {
                return getVariable(expression.getFirstPsiChild());
            }
            return null;
        }
    };
}
Also used : PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) NotNull(org.jetbrains.annotations.NotNull) IElementType(com.intellij.psi.tree.IElementType) BasePhpElementVisitor(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor) PhpScopeHolder(com.jetbrains.php.codeInsight.PhpScopeHolder) Nullable(org.jetbrains.annotations.Nullable) NotNull(org.jetbrains.annotations.NotNull)

Example 3 with PhpEntryPointInstruction

use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction in project phpinspectionsea by kalessil.

the class ParameterImmediateOverrideStrategy method apply.

public static void apply(@NotNull Function function, @NotNull ProblemsHolder holder) {
    /* general requirements for a function */
    final Parameter[] params = function.getParameters();
    final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(function);
    if (null == body || 0 == params.length || 0 == ExpressionSemanticUtil.countExpressionsInGroup(body)) {
        return;
    }
    final PhpEntryPointInstruction start = function.getControlFlow().getEntryPoint();
    for (final Parameter param : params) {
        /* overriding params by reference is totally fine */
        if (param.isPassByRef()) {
            continue;
        }
        final String parameterName = param.getName();
        PhpAccessVariableInstruction[] uses = PhpControlFlowUtil.getFollowingVariableAccessInstructions(start, parameterName, false);
        /* at least 2 uses expected: override and any other operation */
        if (uses.length < 2) {
            continue;
        }
        /* first use should be a write directly in function body */
        final PhpPsiElement expression = uses[0].getAnchor();
        final PsiElement parent = expression.getParent();
        if (OpenapiTypesUtil.isAssignment(parent) && expression == ((AssignmentExpression) parent).getVariable()) {
            /* the assignment must be directly in the body, no conditional/in-loop overrides are checked */
            final PsiElement grandParent = parent.getParent();
            if (null != grandParent && body != grandParent.getParent()) {
                continue;
            }
            int nameHits = 0;
            /* count name hits, to identify if original value was considered */
            final Collection<Variable> vars = PsiTreeUtil.findChildrenOfType(parent, Variable.class);
            for (Variable variable : vars) {
                nameHits += parameterName.equals(variable.getName()) ? 1 : 0;
                if (nameHits > 1) {
                    break;
                }
            }
            vars.clear();
            /* okay, original value 100% lost */
            if (1 == nameHits) {
                holder.registerProblem(expression, message, ProblemHighlightType.GENERIC_ERROR);
            }
        }
    }
}
Also used : PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) PsiElement(com.intellij.psi.PsiElement)

Example 4 with PhpEntryPointInstruction

use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction in project phpinspectionsea by kalessil.

the class ReferenceMismatchInspector method buildVisitor.

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

        /**
         * TODO: checkReferenceReturnedByCallable - ternary operator, argument usages ?
         */
        /* parameters by reference */
        @Override
        public void visitPhpMethod(@NotNull Method method) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel php = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (!php.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                this.checkParameters(method.getParameters(), method);
            }
        }

        @Override
        public void visitPhpFunction(@NotNull Function function) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (phpVersion.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                // PHP7 and newer
                return;
            }
            /* older versions are still affected */
            this.checkParameters(function.getParameters(), function);
        }

        private void checkParameters(Parameter[] arrParameters, Function objScopeHolder) {
            PhpEntryPointInstruction objEntryPoint = objScopeHolder.getControlFlow().getEntryPoint();
            HashSet<PsiElement> emptyReportedItemsRegistry = ReferenceMismatchInspector.getFunctionReportingRegistry(objScopeHolder);
            for (Parameter parameter : arrParameters) {
                /* skip un-discoverable and non-reference parameters */
                String strParameterName = parameter.getName();
                if (!parameter.isPassByRef() || StringUtils.isEmpty(strParameterName)) {
                    continue;
                }
                inspectScopeForReferenceMissUsages(objEntryPoint, strParameterName, emptyReportedItemsRegistry);
            }
            emptyReportedItemsRegistry.clear();
        }

        /* = & variable/property patterns */
        @Override
        public void visitPhpAssignmentExpression(@NotNull AssignmentExpression assignmentExpression) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (phpVersion.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                // PHP7 and newer
                return;
            }
            /* older versions are still affected */
            PsiElement value = assignmentExpression.getValue();
            PsiElement variable = assignmentExpression.getVariable();
            if (variable instanceof Variable && (value instanceof Variable || value instanceof FieldReference || value instanceof ArrayAccessExpression || value instanceof FunctionReference)) {
                String strVariable = ((Variable) variable).getName();
                PsiElement operation = value.getPrevSibling();
                if (operation instanceof PsiWhiteSpace) {
                    operation = operation.getPrevSibling();
                }
                if (!StringUtils.isEmpty(strVariable) && null != operation && operation.getText().replaceAll("\\s+", "").equals("=&")) {
                    /* the case, scan for miss-usages assuming variable is unique */
                    Function scope = ExpressionSemanticUtil.getScope(assignmentExpression);
                    if (null != scope) {
                        // report items, but ensure no duplicated messages
                        HashSet<PsiElement> reportedItemsRegistry = ReferenceMismatchInspector.getFunctionReportingRegistry(scope);
                        inspectScopeForReferenceMissUsages(scope.getControlFlow().getEntryPoint(), strVariable, reportedItemsRegistry);
                    }
                }
            }
        }

        /* assign reference from function */
        @Override
        public void visitPhpMethodReference(@NotNull MethodReference reference) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (phpVersion.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                // PHP7 and newer
                return;
            }
            /* older versions are still affected */
            this.checkReferenceReturnedByCallable(reference);
        }

        @Override
        public void visitPhpFunctionCall(@NotNull FunctionReference reference) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel php = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (!php.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                this.checkReferenceReturnedByCallable(reference);
            }
        }

        /* aggressive foreach optimization when value is reference */
        @Override
        public void visitPhpForeach(@NotNull ForeachStatement foreach) {
            /* PHP7 seems to be ref mismatch free */
            final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(holder.getProject()).getLanguageLevel();
            if (phpVersion.hasFeature(PhpLanguageFeature.SCALAR_TYPE_HINTS)) {
                // PHP7 and newer
                return;
            }
            /* older versions are still affected */
            /* lookup for reference preceding value */
            Variable objForeachValue = foreach.getValue();
            if (null != objForeachValue) {
                String strVariable = objForeachValue.getName();
                PsiElement prevElement = objForeachValue.getPrevSibling();
                if (prevElement instanceof PsiWhiteSpace) {
                    prevElement = prevElement.getPrevSibling();
                }
                if (!StringUtils.isEmpty(strVariable) && OpenapiTypesUtil.is(prevElement, PhpTokenTypes.opBIT_AND)) {
                    /* the case, scan for miss-usages assuming value is unique */
                    Function scope = ExpressionSemanticUtil.getScope(foreach);
                    if (null != scope) {
                        // report items, but ensure no duplicated messages
                        HashSet<PsiElement> reportedItemsRegistry = ReferenceMismatchInspector.getFunctionReportingRegistry(scope);
                        reportedItemsRegistry.add(objForeachValue);
                        inspectScopeForReferenceMissUsages(scope.getControlFlow().getEntryPoint(), strVariable, reportedItemsRegistry);
                    }
                }
            }
        }

        private void inspectScopeForReferenceMissUsages(@NotNull PhpEntryPointInstruction objEntryPoint, @NotNull String strParameterName, @NotNull Set<PsiElement> reportedItemsRegistry) {
            PsiElement previous;
            PsiElement objExpression = null;
            /* find usage inside scope */
            PhpAccessVariableInstruction[] arrUsages = PhpControlFlowUtil.getFollowingVariableAccessInstructions(objEntryPoint, strParameterName, false);
            for (final PhpAccessVariableInstruction objInstruction : arrUsages) {
                previous = objExpression;
                objExpression = objInstruction.getAnchor().getParent();
                /* collided with foreach index/value => bug */
                if (objExpression instanceof ForeachStatement) {
                    final ForeachStatement foreach = (ForeachStatement) objExpression;
                    if (previous instanceof PhpUnset) {
                        break;
                    }
                    final Variable foreachValue = foreach.getValue();
                    if (null != foreachValue && !StringUtils.isEmpty(foreachValue.getName()) && foreachValue.getName().equals(strParameterName)) {
                        if (!reportedItemsRegistry.contains(foreachValue)) {
                            reportedItemsRegistry.add(foreachValue);
                            holder.registerProblem(foreachValue, strErrorForeachIntoReference, ProblemHighlightType.ERROR);
                        }
                        continue;
                    }
                    final Variable foreachKey = foreach.getKey();
                    if (null != foreachKey && !StringUtils.isEmpty(foreachKey.getName()) && foreachKey.getName().equals(strParameterName)) {
                        if (!reportedItemsRegistry.contains(foreachKey)) {
                            reportedItemsRegistry.add(foreachKey);
                            holder.registerProblem(foreachKey, strErrorForeachIntoReference, ProblemHighlightType.ERROR);
                        }
                        continue;
                    }
                }
                /* test if provided as non-reference argument (copy dispatched) */
                if (objExpression instanceof ParameterList && objExpression.getParent() instanceof FunctionReference) {
                    FunctionReference reference = (FunctionReference) objExpression.getParent();
                    /* not resolved or known re-unsafe function */
                    final PsiElement callable = OpenapiResolveUtil.resolveReference(reference);
                    if (!(callable instanceof Function)) {
                        continue;
                    }
                    final String strCallableName = ((Function) callable).getName();
                    if (!StringUtils.isEmpty(strCallableName) && legalizedMismatchingFunctions.contains(strCallableName)) {
                        continue;
                    }
                    /* check if call arguments contains our parameter */
                    int indexInArguments = -1;
                    boolean providedAsArgument = false;
                    for (PsiElement callArgument : reference.getParameters()) {
                        ++indexInArguments;
                        if (callArgument instanceof Variable) {
                            Variable argument = (Variable) callArgument;
                            String argumentName = argument.getName();
                            if (!StringUtils.isEmpty(argumentName) && argumentName.equals(strParameterName)) {
                                providedAsArgument = true;
                                break;
                            }
                        }
                    }
                    /* if not found, keep processing usages */
                    if (!providedAsArgument) {
                        continue;
                    }
                    /* now check what is declared in resolved callable */
                    final Parameter[] usageCallableParameters = ((Function) callable).getParameters();
                    if (usageCallableParameters.length >= indexInArguments + 1) {
                        final Parameter parameter = usageCallableParameters[indexInArguments];
                        if (!parameter.isPassByRef()) {
                            /* additionally try filtering types for reducing false-positives on scalars */
                            final PhpType type = OpenapiResolveUtil.resolveType(parameter, holder.getProject());
                            if (type != null && !PhpType.isSubType(type, legalizedTypesForMismatchingSet)) {
                                final PsiElement target = reference.getParameters()[indexInArguments];
                                if (!reportedItemsRegistry.contains(target)) {
                                    holder.registerProblem(target, "Reference mismatch, copy will be dispatched into function", ProblemHighlightType.WEAK_WARNING);
                                    reportedItemsRegistry.add(target);
                                }
                                continue;
                            }
                        }
                    }
                }
                /* test is assigned to a variable without stating its reference (copy stored) */
                if (objExpression instanceof AssignmentExpression) {
                    /* assignment structure verify */
                    AssignmentExpression assignment = (AssignmentExpression) objExpression;
                    if (assignment.getValue() instanceof Variable) {
                        Variable variable = (Variable) assignment.getValue();
                        String strVariable = variable.getName();
                        /* references parameter */
                        if (!StringUtils.isEmpty(strVariable) && strVariable.equals(strParameterName)) {
                            /* check if assignments states reference usage */
                            PsiElement operation = variable.getPrevSibling();
                            if (operation instanceof PsiWhiteSpace) {
                                operation = operation.getPrevSibling();
                            }
                            /* report if not */
                            if (null != operation && !operation.getText().replaceAll("\\s+", "").equals("=&")) {
                                if (!reportedItemsRegistry.contains(objExpression)) {
                                    holder.registerProblem(objExpression, "Reference mismatch, copy will be stored (for non-objects)", ProblemHighlightType.WEAK_WARNING);
                                    reportedItemsRegistry.add(objExpression);
                                }
                            }
                        }
                    }
                }
            }
        }

        private void checkReferenceReturnedByCallable(@NotNull FunctionReference reference) {
            /* check context before resolving anything  */
            final PsiElement parent = reference.getParent();
            if (parent instanceof AssignmentExpression) {
                /* assignment structure verify */
                final AssignmentExpression assignment = (AssignmentExpression) parent;
                if (assignment.getValue() == reference) {
                    /* try resolving now */
                    final PsiElement callable = OpenapiResolveUtil.resolveReference(reference);
                    if (callable instanceof Function) {
                        /* ensure name discoverable */
                        final Function function = (Function) callable;
                        final PsiElement nameNode = NamedElementUtil.getNameIdentifier(function);
                        if (null != nameNode) {
                            /* is defined like returning reference */
                            PsiElement prevElement = nameNode.getPrevSibling();
                            if (prevElement instanceof PsiWhiteSpace) {
                                prevElement = prevElement.getPrevSibling();
                            }
                            if (OpenapiTypesUtil.is(prevElement, PhpTokenTypes.opBIT_AND)) {
                                /* check if assignments states reference usage */
                                PsiElement operation = reference.getPrevSibling();
                                if (operation instanceof PsiWhiteSpace) {
                                    operation = operation.getPrevSibling();
                                }
                                /* report if not */
                                if (null != operation && !operation.getText().replaceAll("\\s+", "").equals("=&")) {
                                    holder.registerProblem(parent, "Reference mismatch, copy will be stored (for non-objects)", ProblemHighlightType.WEAK_WARNING);
                                }
                            }
                        }
                    }
                }
            }
        }
    };
}
Also used : HashSet(java.util.HashSet) Set(java.util.Set) PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) 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) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpLanguageLevel(com.jetbrains.php.config.PhpLanguageLevel) PsiWhiteSpace(com.intellij.psi.PsiWhiteSpace) NotNull(org.jetbrains.annotations.NotNull)

Example 5 with PhpEntryPointInstruction

use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction in project phpinspectionsea by kalessil.

the class ForeachSourceInspector method buildVisitor.

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

        @Override
        public void visitPhpForeach(@NotNull ForeachStatement foreach) {
            final PsiElement source = ExpressionSemanticUtil.getExpressionTroughParenthesis(foreach.getArray());
            if (source instanceof PhpTypedElement && !isEnsuredByPyParentIf(foreach, source)) {
                this.analyseContainer(source);
            }
        }

        /* should cover is_array/is_iterable in direct parent if of the loop, while PS types resolving gets improved */
        private boolean isEnsuredByPyParentIf(@NotNull ForeachStatement foreach, @NotNull PsiElement source) {
            boolean result = false;
            if (foreach.getPrevPsiSibling() == null) {
                final PsiElement ifCandidate = foreach.getParent() instanceof GroupStatement ? foreach.getParent().getParent() : null;
                final PsiElement conditions;
                if (ifCandidate instanceof If) {
                    conditions = ((If) ifCandidate).getCondition();
                } else if (ifCandidate instanceof ElseIf) {
                    conditions = ((ElseIf) ifCandidate).getCondition();
                } else {
                    conditions = null;
                }
                if (conditions != null) {
                    for (final PsiElement candidate : PsiTreeUtil.findChildrenOfType(conditions, source.getClass())) {
                        if (OpeanapiEquivalenceUtil.areEqual(candidate, source)) {
                            final PsiElement call = candidate.getParent() instanceof ParameterList ? candidate.getParent().getParent() : null;
                            if (OpenapiTypesUtil.isFunctionReference(call)) {
                                final String functionName = ((FunctionReference) call).getName();
                                if (functionName != null && (functionName.equals("is_array") || functionName.equals("is_iterable"))) {
                                    result = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            return result;
        }

        private void analyseContainer(@NotNull PsiElement container) {
            final PhpType resolvedType = OpenapiResolveUtil.resolveType((PhpTypedElement) container, container.getProject());
            if (resolvedType == null) {
                return;
            }
            final Set<String> types = new HashSet<>();
            resolvedType.filterUnknown().getTypes().forEach(t -> types.add(Types.getType(t)));
            if (types.isEmpty()) {
                /* false-positives: pre-defined variables */
                if (container instanceof Variable) {
                    final String variableName = ((Variable) container).getName();
                    if (ExpressionCostEstimateUtil.predefinedVars.contains(variableName)) {
                        return;
                    }
                }
                if (REPORT_UNRECOGNIZED_TYPES) {
                    holder.registerProblem(container, patternNotRecognized, ProblemHighlightType.WEAK_WARNING);
                }
                return;
            }
            /* false-positives: multiple return types checked only in function/method; no global context */
            final PsiElement scope = ExpressionSemanticUtil.getBlockScope(container);
            if (types.size() > 1 && !(scope instanceof Function)) {
                types.clear();
                return;
            }
            /* false-positives: mixed parameter type, parameter overridden before foreach */
            if (types.size() > 1 && scope instanceof Function && container instanceof Variable) {
                final String parameter = ((Variable) container).getName();
                final PhpEntryPointInstruction start = ((Function) scope).getControlFlow().getEntryPoint();
                final PhpAccessVariableInstruction[] uses = PhpControlFlowUtil.getFollowingVariableAccessInstructions(start, parameter, false);
                for (final PhpAccessVariableInstruction instruction : uses) {
                    final PhpPsiElement expression = instruction.getAnchor();
                    /* when matched itself, stop processing */
                    if (expression == container) {
                        break;
                    }
                    final PsiElement parent = expression.getParent();
                    if (parent instanceof AssignmentExpression) {
                        final PsiElement matchCandidate = ((AssignmentExpression) parent).getVariable();
                        if (matchCandidate != null && OpeanapiEquivalenceUtil.areEqual(matchCandidate, container)) {
                            types.clear();
                            return;
                        }
                    }
                }
            }
            /* false-positives: array type parameter declaration adds mixed */
            if (types.size() > 1 && scope instanceof Function && container instanceof ArrayAccessExpression) {
                final PsiElement candidate = ((ArrayAccessExpression) container).getValue();
                if (candidate instanceof Variable && types.contains(Types.strMixed) && types.contains(Types.strArray)) {
                    types.remove(Types.strMixed);
                }
            }
            /* gracefully request to specify exact types which can appear (mixed, object) */
            if (types.contains(Types.strMixed)) {
                /* false-positive: mixed definitions from stub functions */
                boolean isStubFunction = false;
                if (OpenapiTypesUtil.isFunctionReference(container)) {
                    final PsiElement function = OpenapiResolveUtil.resolveReference((FunctionReference) container);
                    final String filePath = function == null ? null : function.getContainingFile().getVirtualFile().getCanonicalPath();
                    isStubFunction = filePath != null && filePath.contains(".jar!") && filePath.contains("/stubs/");
                }
                /* false-positive: mixed definition from array type */
                if (!isStubFunction && !types.contains(Types.strArray) && REPORT_MIXED_TYPES) {
                    final String message = String.format(patternMixedTypes, Types.strMixed);
                    holder.registerProblem(container, message, ProblemHighlightType.WEAK_WARNING);
                }
                types.remove(Types.strMixed);
            }
            if (types.contains(Types.strObject)) {
                if (REPORT_MIXED_TYPES) {
                    final String message = String.format(patternMixedTypes, Types.strObject);
                    holder.registerProblem(container, message, ProblemHighlightType.WEAK_WARNING);
                }
                types.remove(Types.strObject);
            }
            /* respect patter when returned array and bool|null for indicating failures*/
            if (types.size() == 2 && types.contains(Types.strArray)) {
                types.remove(Types.strBoolean);
                types.remove(Types.strNull);
            }
            /* do not process foreach-compatible types */
            types.remove(Types.strArray);
            types.remove(Types.strIterable);
            types.remove("\\Traversable");
            types.remove("\\Iterator");
            types.remove("\\IteratorAggregate");
            /* don't process mysterious empty set type */
            types.remove(Types.strEmptySet);
            /* iterate rest of types */
            if (!types.isEmpty()) {
                final PhpIndex index = PhpIndex.getInstance(holder.getProject());
                for (final String type : types) {
                    /* report if scalar type is met */
                    if (!type.startsWith("\\")) {
                        holder.registerProblem(container, String.format(patternScalar, type), ProblemHighlightType.GENERIC_ERROR);
                        continue;
                    }
                    /* check classes for the Traversable interface in the inheritance chain */
                    final List<PhpClass> classes = OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(type, index);
                    if (!classes.isEmpty()) {
                        boolean hasTraversable = false;
                        for (final PhpClass clazz : classes) {
                            final Set<PhpClass> interfaces = InterfacesExtractUtil.getCrawlInheritanceTree(clazz, false);
                            if (!interfaces.isEmpty()) {
                                hasTraversable = interfaces.stream().anyMatch(i -> i.getFQN().equals("\\Traversable"));
                                interfaces.clear();
                                if (hasTraversable) {
                                    break;
                                }
                            }
                        }
                        classes.clear();
                        if (!hasTraversable) {
                            holder.registerProblem(container, String.format(patternObject, type));
                        }
                    }
                }
                types.clear();
            }
        }
    };
}
Also used : PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) BasePhpInspection(com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection) com.jetbrains.php.lang.psi.elements(com.jetbrains.php.lang.psi.elements) InterfacesExtractUtil(com.kalessil.phpStorm.phpInspectionsEA.utils.hierarhy.InterfacesExtractUtil) Set(java.util.Set) ExpressionCostEstimateUtil(com.kalessil.phpStorm.phpInspectionsEA.inspectors.ifs.utils.ExpressionCostEstimateUtil) PhpIndex(com.jetbrains.php.PhpIndex) OptionsComponent(com.kalessil.phpStorm.phpInspectionsEA.options.OptionsComponent) PhpControlFlowUtil(com.jetbrains.php.codeInsight.controlFlow.PhpControlFlowUtil) 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) 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) PhpEntryPointInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction) 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) HashSet(java.util.HashSet) PhpAccessVariableInstruction(com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction) PhpIndex(com.jetbrains.php.PhpIndex) NotNull(org.jetbrains.annotations.NotNull)

Aggregations

PhpAccessVariableInstruction (com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction)6 PhpEntryPointInstruction (com.jetbrains.php.codeInsight.controlFlow.instructions.PhpEntryPointInstruction)6 PsiElement (com.intellij.psi.PsiElement)5 BasePhpElementVisitor (com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor)5 NotNull (org.jetbrains.annotations.NotNull)5 PhpScopeHolder (com.jetbrains.php.codeInsight.PhpScopeHolder)3 PhpType (com.jetbrains.php.lang.psi.resolve.types.PhpType)3 HashSet (java.util.HashSet)3 Set (java.util.Set)3 ProblemsHolder (com.intellij.codeInspection.ProblemsHolder)2 PsiElementVisitor (com.intellij.psi.PsiElementVisitor)2 PhpIndex (com.jetbrains.php.PhpIndex)2 PhpControlFlowUtil (com.jetbrains.php.codeInsight.controlFlow.PhpControlFlowUtil)2 com.jetbrains.php.lang.psi.elements (com.jetbrains.php.lang.psi.elements)2 BasePhpInspection (com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection)2 com.kalessil.phpStorm.phpInspectionsEA.utils (com.kalessil.phpStorm.phpInspectionsEA.utils)2 InterfacesExtractUtil (com.kalessil.phpStorm.phpInspectionsEA.utils.hierarhy.InterfacesExtractUtil)2 ProblemHighlightType (com.intellij.codeInspection.ProblemHighlightType)1 Project (com.intellij.openapi.project.Project)1 PsiWhiteSpace (com.intellij.psi.PsiWhiteSpace)1