use of com.jetbrains.php.PhpIndex in project phpinspectionsea by kalessil.
the class ClassInStringContextStrategy method apply.
public static boolean apply(@Nullable PsiElement nonStringOperand, @NotNull ProblemsHolder holder, @NotNull PsiElement expression, @NotNull String classHasNoToStringMessage) {
if (null == nonStringOperand) {
return false;
}
final Set<String> resolvedTypes = new HashSet<>();
if (nonStringOperand instanceof PhpTypedElement) {
final PhpType resolved = OpenapiResolveUtil.resolveType((PhpTypedElement) nonStringOperand, holder.getProject());
if (resolved != null) {
resolved.filterUnknown().getTypes().forEach(t -> resolvedTypes.add(Types.getType(t)));
}
}
if (!TypesSemanticsUtil.isNullableObjectInterface(resolvedTypes)) {
resolvedTypes.clear();
return false;
}
/* collect classes to check if __toString() is there */
final PhpIndex index = PhpIndex.getInstance(holder.getProject());
final List<PhpClass> listClasses = new ArrayList<>();
resolvedTypes.stream().filter(fqn -> fqn.charAt(0) == '\\').forEach(fqn -> listClasses.addAll(OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(fqn, index)));
resolvedTypes.clear();
/* check methods, error on first one violated requirements */
for (final PhpClass clazz : listClasses) {
if (OpenapiResolveUtil.resolveMethod(clazz, "__toString") == null) {
holder.registerProblem(expression, MessagesPresentationUtil.prefixWithEa(classHasNoToStringMessage.replace("%class%", clazz.getFQN())), ProblemHighlightType.ERROR);
break;
}
}
/* terminate inspection, php will call __toString() */
listClasses.clear();
return true;
}
use of com.jetbrains.php.PhpIndex 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) {
if (!this.isTestContext(method)) {
this.inspectUsages(method.getParameters(), method);
}
}
@Override
public void visitPhpFunction(@NotNull Function function) {
if (!this.isTestContext(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<>();
final PhpType parameterType = OpenapiResolveUtil.resolveType(parameter, project);
if (parameterType != null) {
label: for (final String type : parameterType.filterUnknown().getTypes()) {
final String typeNormalized = Types.getType(type);
switch(typeNormalized) {
case Types.strMixed:
case Types.strObject:
paramTypes.clear();
break label;
case Types.strCallable:
paramTypes.add(Types.strArray);
paramTypes.add(Types.strString);
paramTypes.add("\\Closure");
break;
case Types.strIterable:
paramTypes.add(Types.strArray);
paramTypes.add("\\Traversable");
break;
}
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 (PhpLanguageUtil.isNull(defaultValue)) {
continue;
}
}
/* now find instructions operating on the parameter and perform analysis */
final String parameterName = parameter.getName();
for (final PhpAccessVariableInstruction instruction : OpenapiControlFlowUtil.getFollowingVariableAccessInstructions(entryPoint, parameterName)) {
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":
if (paramTypes.contains(Types.strString)) {
continue;
}
isTypeAnnounced = paramTypes.contains(Types.strNumber) || 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.contains(Types.strCallable) || paramTypes.stream().anyMatch(t -> classReferences.contains(t) || (t.startsWith("\\") && !t.equals("\\Closure")));
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, MessagesPresentationUtil.prefixWithEa(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|array & false|null */
if (resolved.contains(Types.strString) || resolved.contains(Types.strArray)) {
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);
}
}
} else /* false-positives: nullable objects */
if (resolved.contains(Types.strNull)) {
final boolean isNullableObject = paramTypes.stream().anyMatch(t -> classReferences.contains(t) || t.startsWith("\\") && !t.equals("\\Closure"));
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 && OpenapiEquivalenceUtil.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());
final int filteredTypesCount = filteredTypes.size();
/* clear resolved class or interface + class */
if (filteredTypesCount == 1 || filteredTypesCount == 2) {
type = filteredTypes.iterator().next();
}
filteredTypes.clear();
}
}
}
/* translate static/self into FQNs didn't work, skip */
if (classReferences.contains(type)) {
continue;
}
}
final boolean isViolation = !this.isTypeCompatibleWith(type, paramTypes, index);
if (isViolation) {
holder.registerProblem(value, String.format(MessagesPresentationUtil.prefixWithEa(patternViolationInAssignment), type));
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);
}
};
}
use of com.jetbrains.php.PhpIndex in project phpinspectionsea by kalessil.
the class OffsetOperationsInspector method isContainerSupportsArrayAccess.
private boolean isContainerSupportsArrayAccess(@NotNull Project project, @NotNull ArrayAccessExpression expression, @NotNull Set<String> indexTypesSupported) {
// ok JB parses `$var[]= ...` always as array, lets make it working properly and report them later
final PsiElement container = expression.getValue();
if (null == container) {
return false;
}
final Set<String> containerTypes = new HashSet<>();
if (container instanceof PhpTypedElement) {
final PhpType type = OpenapiResolveUtil.resolveType((PhpTypedElement) container, project);
if (type != null && !type.hasUnknown()) {
type.getTypes().forEach(t -> containerTypes.add(Types.getType(t)));
Stream.of(Types.strSelf, Types.strStatic).forEach(containerTypes::remove);
}
}
/* === cleanup resolved types === */
if (containerTypes.contains(Types.strMixed)) {
// mixed are not analyzable
containerTypes.clear();
return true;
}
if (containerTypes.size() == 2 && containerTypes.contains(Types.strString)) {
final boolean isForeachKeyType = containerTypes.contains(Types.strInteger);
final boolean isCoreApiStringType = containerTypes.contains(Types.strBoolean);
if (isForeachKeyType || isCoreApiStringType) {
containerTypes.clear();
return true;
}
}
if (containerTypes.contains(Types.strCallable)) {
// treat callable as array
containerTypes.remove(Types.strCallable);
containerTypes.add(Types.strArray);
containerTypes.add(Types.strString);
}
// don't process nulls
containerTypes.remove(Types.strNull);
// don't process generalized objects
containerTypes.remove(Types.strObject);
// don't process mysterious empty set type
containerTypes.remove(Types.strEmptySet);
/* === if we could not resolve container, do nothing === */
if (containerTypes.isEmpty()) {
return true;
}
final PhpIndex index = PhpIndex.getInstance(project);
boolean supportsOffsets = false;
for (final String typeToCheck : containerTypes) {
// commonly used case: string and array
if (typeToCheck.equals(Types.strArray) || typeToCheck.equals(Types.strString)) {
supportsOffsets = true;
/* here we state which regular index types we want to promote */
indexTypesSupported.add(Types.strString);
indexTypesSupported.add(Types.strInteger);
continue;
}
// some of possible types are scalars, what's wrong
if (!typeToCheck.isEmpty() && typeToCheck.charAt(0) != '\\') {
supportsOffsets = false;
break;
}
// now we are at point when analyzing classes only
final List<PhpClass> classes = OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(typeToCheck, index);
for (final PhpClass clazz : classes) {
/* custom offsets management, follow annotated types */
for (final String methodName : Arrays.asList("offsetGet", "offsetSet", "__get", "__set")) {
final Method method = OpenapiResolveUtil.resolveMethod(clazz, methodName);
if (method != null) {
/* regular array index types can be applied */
if (methodName.startsWith("__")) {
indexTypesSupported.add(Types.strString);
indexTypesSupported.add(Types.strInteger);
} else /* user-defined index types can be applied */
{
final Parameter[] parameters = method.getParameters();
if (parameters.length > 0) {
final PhpType type = OpenapiResolveUtil.resolveType(parameters[0], project);
if (type != null) {
type.filterUnknown().getTypes().forEach(t -> indexTypesSupported.add(Types.getType(t)));
}
}
}
supportsOffsets = true;
}
}
}
classes.clear();
}
// when might not support offset access, reuse types container to report back why
if (!supportsOffsets) {
indexTypesSupported.clear();
indexTypesSupported.addAll(containerTypes);
}
containerTypes.clear();
return supportsOffsets;
}
use of com.jetbrains.php.PhpIndex in project phpinspectionsea by kalessil.
the class MissingIssetImplementationInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpEmpty(@NotNull PhpEmpty expression) {
this.analyzeDispatchedExpressions(expression.getVariables());
}
@Override
public void visitPhpIsset(@NotNull PhpIsset expression) {
this.analyzeDispatchedExpressions(expression.getVariables());
}
private void analyzeDispatchedExpressions(@NotNull PhpExpression[] parameters) {
final Project project = holder.getProject();
final PhpIndex projectIndex = PhpIndex.getInstance(project);
for (final PhpExpression parameter : parameters) {
if (parameter instanceof FieldReference) {
final FieldReference reference = (FieldReference) parameter;
final ASTNode nameNode = reference.getNameNode();
final String parameterName = parameter.getName();
/* if the field name is not implicit or the field resolved, continue */
if ((nameNode == null || nameNode instanceof Variable) || (parameterName == null || parameterName.isEmpty()) || !OpenapiTypesUtil.is(OpenapiPsiSearchUtil.findResolutionOperator(reference), PhpTokenTypes.ARROW) || OpenapiResolveUtil.resolveReference(reference) != null) {
continue;
}
/* false-positives: in the $this context we are dealing with dynamic properties */
final PhpExpression variable = reference.getClassReference();
if (null == variable || variable.getText().equals("$this")) {
continue;
}
/* long way around: identify an lookup classes */
final Set<String> resolvedTypes = new HashSet<>();
final PhpType resolved = OpenapiResolveUtil.resolveType(variable, project);
if (resolved != null) {
resolved.filterUnknown().getTypes().forEach(t -> resolvedTypes.add(Types.getType(t)));
}
for (final String type : resolvedTypes) {
/* false-positives: SimpleXMLElement, stdClass */
if (type.startsWith("\\") && !magicClasses.contains(type)) {
final Collection<PhpClass> classes = projectIndex.getClassesByFQN(type);
final PhpClass clazz = classes.isEmpty() ? null : classes.iterator().next();
/* resolved class FQN might differ from what type states */
if (clazz != null && !magicClasses.contains(clazz.getFQN())) {
final boolean hasField = OpenapiResolveUtil.resolveField(clazz, parameterName) != null;
if (!hasField && OpenapiResolveUtil.resolveMethod(clazz, "__isset") == null) {
holder.registerProblem(parameter, MessagesPresentationUtil.prefixWithEa(messagePattern.replace("%c%", type)), ProblemHighlightType.GENERIC_ERROR);
break;
}
}
}
}
resolvedTypes.clear();
}
}
}
};
}
use of com.jetbrains.php.PhpIndex in project phpinspectionsea by kalessil.
the class TypeUnsafeComparisonInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpBinaryExpression(@NotNull BinaryExpression expression) {
final IElementType operator = expression.getOperationType();
if (operator == PhpTokenTypes.opEQUAL || operator == PhpTokenTypes.opNOT_EQUAL) {
this.analyze(expression, operator);
}
}
private void analyze(@NotNull final BinaryExpression subject, @NotNull final IElementType operator) {
final String targetOperator = PhpTokenTypes.opEQUAL == operator ? "===" : "!==";
final PsiElement left = subject.getLeftOperand();
final PsiElement right = subject.getRightOperand();
if (right instanceof StringLiteralExpression || left instanceof StringLiteralExpression) {
final PsiElement nonStringOperand;
final String literalValue;
if (right instanceof StringLiteralExpression) {
literalValue = ((StringLiteralExpression) right).getContents();
nonStringOperand = ExpressionSemanticUtil.getExpressionTroughParenthesis(left);
} else {
literalValue = ((StringLiteralExpression) left).getContents();
nonStringOperand = ExpressionSemanticUtil.getExpressionTroughParenthesis(right);
}
/* resolve 2nd operand type, if class ensure __toString is implemented */
if (ClassInStringContextStrategy.apply(nonStringOperand, holder, subject, messageToStringMethodMissing)) {
/* TODO: weak warning regarding under-the-hood string casting */
return;
}
/* string literal is numeric or empty, no strict compare possible */
if (!literalValue.isEmpty() && !literalValue.matches("^[+-]?[0-9]*\\.?[0-9]+$")) {
holder.registerProblem(subject, String.format(MessagesPresentationUtil.prefixWithEa(patternCompareStrict), targetOperator), new CompareStrictFix(targetOperator));
return;
}
}
/* some objects supporting direct comparison: search for .compare_objects in PHP sources */
if (left != null && right != null) {
final boolean isComparableObject = this.isComparableObject(left) || this.isComparableObject(right);
if (!isComparableObject) {
holder.registerProblem(subject, String.format(MessagesPresentationUtil.prefixWithEa(patternHarden), targetOperator), ProblemHighlightType.WEAK_WARNING);
}
}
}
private boolean isComparableObject(@NotNull PsiElement operand) {
if (operand instanceof PhpTypedElement) {
final Project project = holder.getProject();
final PhpType resolved = OpenapiResolveUtil.resolveType((PhpTypedElement) operand, project);
if (resolved != null) {
final PhpIndex index = PhpIndex.getInstance(project);
final Set<PhpClass> classes = new HashSet<>();
resolved.filterUnknown().getTypes().stream().filter(t -> t.charAt(0) == '\\').forEach(t -> classes.addAll(OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(Types.getType(t), index)));
for (final PhpClass clazz : classes) {
final boolean hasAny = comparable.contains(clazz.getFQN()) || InterfacesExtractUtil.getCrawlInheritanceTree(clazz, true).stream().anyMatch(c -> comparable.contains(c.getFQN()));
if (hasAny) {
classes.clear();
return true;
}
}
classes.clear();
}
}
return false;
}
};
}
Aggregations