use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction 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;
}
};
}
use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction in project phpinspectionsea by kalessil.
the class UselessUnsetInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, final boolean isOnTheFly) {
/* foreach is also a case, but there is no way to get flow entry point in actual JB platform API */
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 scope) {
if (parameters.length > 0) {
final PhpEntryPointInstruction entry = scope.getControlFlow().getEntryPoint();
for (final Parameter parameter : parameters) {
final String parameterName = parameter.getName();
if (!parameterName.isEmpty()) {
for (final PhpAccessVariableInstruction usage : OpenapiControlFlowUtil.getFollowingVariableAccessInstructions(entry, parameterName)) {
final PsiElement expression = usage.getAnchor();
final PsiElement parent = expression.getParent();
if (parent instanceof PhpUnset) {
int unsetParametersCount = ((PhpUnset) parent).getArguments().length;
final PsiElement target = (unsetParametersCount == 1 ? parent : expression);
holder.registerProblem(target, MessagesPresentationUtil.prefixWithEa(message), ProblemHighlightType.LIKE_UNUSED_SYMBOL);
}
}
}
}
}
}
};
}
use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction 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);
}
}
}
}
}
}
}
};
}
use of com.jetbrains.php.codeInsight.controlFlow.instructions.PhpAccessVariableInstruction 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();
}
}
};
}
Aggregations