use of com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor in project phpinspectionsea by kalessil.
the class ProperNullCoalescingOperatorUsageInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpBinaryExpression(@NotNull BinaryExpression binary) {
if (binary.getOperationType() == PhpTokenTypes.opCOALESCE && !this.isPartOfCoalesce(binary) && !this.isTypeCasted(binary)) {
final PsiElement left = binary.getLeftOperand();
final PsiElement right = binary.getRightOperand();
if (left != null && right != null) {
/* case: `call() ?? null` */
if (PhpLanguageUtil.isNull(right)) {
if (left instanceof FunctionReference) {
holder.registerProblem(binary, String.format(MessagesPresentationUtil.prefixWithEa(messageSimplify), left.getText()), new UseLeftOperandFix(left.getText()));
}
return;
}
/* case: `returns_string_or_null() ?? []` */
if (ANALYZE_TYPES && left instanceof PhpTypedElement && right instanceof PhpTypedElement) {
final Function scope = ExpressionSemanticUtil.getScope(binary);
if (scope != null) {
final Set<String> leftTypes = this.resolve((PhpTypedElement) left);
if (leftTypes != null && !leftTypes.isEmpty()) {
final Set<String> rightTypes = this.resolve((PhpTypedElement) right);
if (rightTypes != null && !rightTypes.isEmpty()) {
final boolean complimentary = ALLOW_OVERLAPPING_TYPES ? rightTypes.stream().anyMatch(leftTypes::contains) : rightTypes.containsAll(leftTypes);
if (!complimentary && !this.areRelated(rightTypes, leftTypes)) {
holder.registerProblem(binary, String.format(MessagesPresentationUtil.prefixWithEa(messageMismatch), leftTypes.toString(), rightTypes.toString()));
}
rightTypes.clear();
}
leftTypes.clear();
}
}
}
}
}
}
private boolean isTypeCasted(@NotNull BinaryExpression binary) {
final PsiElement parent = binary.getParent();
if (parent instanceof ParenthesizedExpression) {
final PsiElement grandParent = parent.getParent();
if (grandParent instanceof UnaryExpression) {
final PsiElement operator = ((UnaryExpression) grandParent).getOperation();
return operator != null && PhpTokenTypes.tsCAST_OPS.contains(operator.getNode().getElementType());
}
}
return false;
}
private boolean isPartOfCoalesce(@NotNull BinaryExpression binary) {
final PsiElement parent = binary.getParent();
return parent instanceof BinaryExpression && ((BinaryExpression) parent).getOperationType() == PhpTokenTypes.opCOALESCE;
}
private boolean areRelated(@NotNull Set<String> rightTypes, @NotNull Set<String> leftTypes) {
final Set<PhpClass> left = this.extractClasses(leftTypes);
if (!left.isEmpty()) {
final Set<PhpClass> right = this.extractClasses(rightTypes);
if (!right.isEmpty() && left.stream().anyMatch(right::contains)) {
left.clear();
right.clear();
return true;
}
left.clear();
}
return false;
}
private HashSet<PhpClass> extractClasses(@NotNull Set<String> types) {
final HashSet<PhpClass> classes = new HashSet<>();
final PhpIndex index = PhpIndex.getInstance(holder.getProject());
types.stream().filter(t -> t.startsWith("\\")).forEach(t -> OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(t, index).forEach(c -> classes.addAll(InterfacesExtractUtil.getCrawlInheritanceTree(c, true))));
return classes;
}
@Nullable
private Set<String> resolve(@NotNull PhpTypedElement subject) {
final PhpType type = OpenapiResolveUtil.resolveType(subject, holder.getProject());
if (type != null && !type.hasUnknown()) {
final Set<String> types = type.getTypes().stream().map(Types::getType).collect(Collectors.toSet());
if (!types.isEmpty() && !types.contains(Types.strMixed) && !types.contains(Types.strObject)) {
types.remove(Types.strStatic);
types.remove(Types.strNull);
return types;
}
types.clear();
}
return null;
}
};
}
use of com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor in project phpinspectionsea by kalessil.
the class ClassConstantCanBeUsedInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpFunctionCall(@NotNull FunctionReference reference) {
if (PhpLanguageLevel.get(holder.getProject()).atLeast(PhpLanguageLevel.PHP550)) {
final String functionName = reference.getName();
if (functionName != null) {
if (functionName.equals("get_called_class")) {
final PsiElement[] arguments = reference.getParameters();
if (arguments.length == 0) {
holder.registerProblem(reference, MessagesPresentationUtil.prefixWithEa(messageUseStatic), new UseStaticClassConstantFix());
}
} else if (functionName.equals("get_parent_class")) {
final PsiElement[] arguments = reference.getParameters();
if (arguments.length == 0 && PhpLanguageLevel.get(holder.getProject()).atLeast(PhpLanguageLevel.PHP550)) {
holder.registerProblem(reference, MessagesPresentationUtil.prefixWithEa(messageUseParent), new UseParentClassConstantFix());
}
}
}
}
}
@Override
public void visitPhpStringLiteralExpression(@NotNull StringLiteralExpression expression) {
/* ensure selected language level supports the ::class feature*/
final Project project = holder.getProject();
if (PhpLanguageLevel.get(project).below(PhpLanguageLevel.PHP550)) {
return;
}
/* Skip certain contexts processing and strings with inline injections */
if (!OpenapiTypesUtil.isString(expression) || expression.getFirstPsiChild() != null) {
return;
}
final PsiElement parent = expression.getParent();
if (parent instanceof BinaryExpression) {
boolean process = false;
final BinaryExpression binary = (BinaryExpression) parent;
final PsiElement left = binary.getLeftOperand();
if (binary.getOperationType() == PhpTokenTypes.opCONCAT && left instanceof ConstantReference) {
final String constantName = ((ConstantReference) left).getName();
process = constantName != null && constantName.equals("__NAMESPACE__");
}
if (!process) {
return;
}
}
if (parent instanceof SelfAssignmentExpression || this.isClassAlias(expression)) {
return;
}
/* Process if has no inline statements and at least 3 chars long (foo, bar and etc. are not a case) */
final String contents = this.populateLiteralContent(expression, parent instanceof BinaryExpression);
if (contents.length() > 3 && classNameRegex.matcher(contents).matches()) {
/* do not process lowercase-only strings */
if (contents.indexOf('\\') == -1 && contents.toLowerCase().equals(contents)) {
return;
}
String normalizedContents = contents.replaceAll("\\\\\\\\", "\\\\");
final boolean isFull = normalizedContents.charAt(0) == '\\';
final Set<String> namesToLookup = new HashSet<>();
if (isFull) {
namesToLookup.add(normalizedContents);
} else {
if (LOOK_ROOT_NS_UP || normalizedContents.contains("\\")) {
normalizedContents = '\\' + normalizedContents;
namesToLookup.add(normalizedContents);
}
}
/* if we could find an appropriate candidate and resolved the class => report (case must match) */
if (1 == namesToLookup.size()) {
final String fqn = namesToLookup.iterator().next();
final PhpIndex index = PhpIndex.getInstance(project);
final List<PhpClass> classes = OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(fqn, index);
/* check resolved items */
if (!classes.isEmpty()) {
if (1 == classes.size() && classes.get(0).getFQN().equals(fqn)) {
if (parent instanceof BinaryExpression) {
normalizedContents = expression.getContents().replaceAll("\\\\\\\\", "\\\\").replaceAll("^\\\\", "");
holder.registerProblem(parent, MessagesPresentationUtil.prefixWithEa(messagePattern.replace("%c%", normalizedContents)), new TheLocalFix(normalizedContents, IMPORT_CLASSES_ON_QF, USE_RELATIVE_QF));
} else {
holder.registerProblem(expression, MessagesPresentationUtil.prefixWithEa(messagePattern.replace("%c%", normalizedContents)), new TheLocalFix(normalizedContents, IMPORT_CLASSES_ON_QF, USE_RELATIVE_QF));
}
}
classes.clear();
}
}
namesToLookup.clear();
}
}
@NotNull
private String populateLiteralContent(@NotNull StringLiteralExpression literal, boolean prependNamespace) {
String content = literal.getContents();
if (prependNamespace) {
final PsiElement clazz = PsiTreeUtil.findFirstParent(literal, PARENT_CLASS);
if (clazz != null) {
content = ((PhpClass) clazz).getNamespaceName() + content.replaceAll("^\\\\", "");
}
}
return content;
}
private boolean isClassAlias(@NotNull StringLiteralExpression literal) {
boolean result = false;
final PsiElement parent = literal.getParent();
if (parent instanceof ParameterList) {
final PsiElement grandParent = parent.getParent();
if (OpenapiTypesUtil.isFunctionReference(grandParent)) {
final FunctionReference reference = (FunctionReference) grandParent;
final String functionName = reference.getName();
if (functionName != null && functionName.equals("class_alias")) {
final PsiElement[] arguments = reference.getParameters();
result = arguments.length == 2 && arguments[1] == literal;
}
}
}
return result;
}
};
}
use of com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor in project phpinspectionsea by kalessil.
the class PackedHashtableOptimizationInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
/* TODO: docs, http://blog.jpauli.tech/2016/04/08/hashtables.html#packed-hashtable-optimization */
@Override
public void visitPhpArrayCreationExpression(@NotNull ArrayCreationExpression expression) {
/* requires PHP7 */
if (PhpLanguageLevel.get(holder.getProject()).below(PhpLanguageLevel.PHP700)) {
return;
}
/* requires at least 3 children - let array to grow enough */
final PsiElement[] children = expression.getChildren();
if (children.length < 3) {
return;
}
/* false-positives: test classes */
if (this.isTestContext(expression)) {
return;
}
/* step 1: collect indexes and verify array structure */
final List<PhpPsiElement> indexes = new ArrayList<>();
for (final PsiElement pairCandidate : children) {
if (pairCandidate instanceof ArrayHashElement) {
final PhpPsiElement key = ((ArrayHashElement) pairCandidate).getKey();
if ((key instanceof StringLiteralExpression && key.getFirstPsiChild() == null) || OpenapiTypesUtil.isNumber(key)) {
indexes.add(key);
continue;
}
}
break;
}
if (indexes.size() != children.length) {
indexes.clear();
return;
}
/* step 2: analyze collected indexes */
// if string literal is not numeric => stop
boolean hasStringIndexes = false;
boolean hasIncreasingIndexes = true;
int lastIndex = Integer.MIN_VALUE;
for (PhpPsiElement index : indexes) {
final String numericIndex;
final int integerIndex;
/* extract text representation of the index */
if (index instanceof StringLiteralExpression) {
hasStringIndexes = true;
numericIndex = ((StringLiteralExpression) index).getContents();
/* '01' and etc cases can not be converted */
if (numericIndex.length() > 1 && '0' == numericIndex.charAt(0)) {
indexes.clear();
return;
}
} else {
numericIndex = index.getText().replaceAll("\\s+", "");
}
/* try converting into integer */
try {
integerIndex = Integer.parseInt(numericIndex);
} catch (NumberFormatException error) {
indexes.clear();
return;
}
if (integerIndex < lastIndex) {
hasIncreasingIndexes = false;
}
lastIndex = integerIndex;
}
/* report if criteria are met */
if (!hasIncreasingIndexes) {
holder.registerProblem(expression.getFirstChild(), MessagesPresentationUtil.prefixWithEa(messageReorder));
}
if (hasIncreasingIndexes && hasStringIndexes) {
holder.registerProblem(expression.getFirstChild(), MessagesPresentationUtil.prefixWithEa(messageUseNumericKeys));
}
}
};
}
use of com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor in project phpinspectionsea by kalessil.
the class CompactArgumentsInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpFunctionCall(@NotNull FunctionReference reference) {
final String functionName = reference.getName();
if (functionName != null && functionName.equals("compact")) {
final PsiElement[] arguments = reference.getParameters();
if (arguments.length > 0) {
final Function scope = ExpressionSemanticUtil.getScope(reference);
if (scope != null) {
/* extract variables names needed */
final Map<String, PsiElement> compactedVariables = new HashMap<>();
for (final PsiElement argument : arguments) {
if (argument instanceof StringLiteralExpression) {
final StringLiteralExpression expression = (StringLiteralExpression) argument;
final String name = expression.getContents();
if (!name.isEmpty() && expression.getFirstPsiChild() == null) {
compactedVariables.put(name, argument);
}
}
}
/* if we have something to analyze, collect what scope provides */
if (!compactedVariables.isEmpty()) {
/* parameters and local variables can be compacted, just ensure the order is correct */
final Set<String> declaredVariables = Arrays.stream(scope.getParameters()).map(Parameter::getName).collect(Collectors.toSet());
for (final PhpReference entry : PsiTreeUtil.findChildrenOfAnyType(scope, Variable.class, FunctionReference.class)) {
if (entry == reference) {
break;
} else if (entry instanceof Variable) {
declaredVariables.add(entry.getName());
}
}
/* analyze and report suspicious parameters, release refs afterwards */
compactedVariables.keySet().forEach(subject -> {
if (!declaredVariables.contains(subject)) {
holder.registerProblem(compactedVariables.get(subject), String.format(MessagesPresentationUtil.prefixWithEa(messagePattern), subject), ProblemHighlightType.GENERIC_ERROR);
}
});
declaredVariables.clear();
compactedVariables.clear();
}
}
}
}
}
};
}
use of com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor in project phpinspectionsea by kalessil.
the class IsEmptyFunctionUsageInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpEmpty(@NotNull PhpEmpty emptyExpression) {
final PhpExpression[] values = emptyExpression.getVariables();
if (values.length == 1) {
final PsiElement subject = ExpressionSemanticUtil.getExpressionTroughParenthesis(values[0]);
if (subject == null || subject instanceof ArrayAccessExpression) {
/* currently, php docs lacks of array structure notations, skip it */
return;
}
final PsiElement parent = emptyExpression.getParent();
final PsiElement operation = parent instanceof UnaryExpression ? ((UnaryExpression) parent).getOperation() : null;
final boolean isInverted = OpenapiTypesUtil.is(operation, PhpTokenTypes.opNOT);
/* extract types */
final Set<String> resolvedTypes = new HashSet<>();
if (subject instanceof PhpTypedElement) {
final PhpType resolved = OpenapiResolveUtil.resolveType((PhpTypedElement) subject, holder.getProject());
if (resolved != null) {
resolved.filterUnknown().getTypes().forEach(t -> resolvedTypes.add(Types.getType(t)));
}
}
/* Case 1: empty(array) - hidden logic - empty array */
if (SUGGEST_TO_USE_COUNT_CHECK && this.isCountableType(resolvedTypes)) {
final String comparison = isInverted ? "!==" : "===";
final String replacement = ComparisonStyle.isRegular() ? String.format("count(%s) %s 0", subject.getText(), comparison) : String.format("0 %s count(%s)", comparison, subject.getText());
final PsiElement target = isInverted ? parent : emptyExpression;
holder.registerProblem(target, String.format(MessagesPresentationUtil.prefixWithEa(patternAlternative), replacement), new UseCountFix(replacement));
resolvedTypes.clear();
return;
}
/* case 2: nullable classes, nullable target core types */
if (SUGGEST_TO_USE_NULL_COMPARISON && ((SUGGEST_NULL_COMPARISON_FOR_SCALARS && this.isNullableCoreType(resolvedTypes)) || TypesSemanticsUtil.isNullableObjectInterface(resolvedTypes))) {
/* false-positive: a field reference used in the subject expression */
PsiElement base = subject;
while (base instanceof PhpPsiElement) {
if (base instanceof FieldReference) {
break;
}
base = ((PhpPsiElement) base).getFirstPsiChild();
}
if (!(base instanceof FieldReference)) {
final String comparison = isInverted ? "!==" : "===";
final String replacement = ComparisonStyle.isRegular() ? String.format("%s %s null", subject.getText(), comparison) : String.format("null %s %s", comparison, subject.getText());
holder.registerProblem(isInverted ? parent : emptyExpression, String.format(MessagesPresentationUtil.prefixWithEa(patternAlternative), replacement), new CompareToNullFix(replacement));
}
resolvedTypes.clear();
return;
}
resolvedTypes.clear();
}
if (REPORT_EMPTY_USAGE) {
holder.registerProblem(emptyExpression, MessagesPresentationUtil.prefixWithEa(messageDoNotUse));
}
}
private boolean isCountableType(@NotNull Set<String> resolvedTypesSet) {
if (!resolvedTypesSet.isEmpty()) {
return resolvedTypesSet.stream().allMatch(t -> {
boolean isIterable = false;
if (t.equals(Types.strArray)) {
isIterable = true;
} else if (t.startsWith("\\")) {
final List<PhpClass> resolved = OpenapiResolveUtil.resolveClassesAndInterfacesByFQN(t, PhpIndex.getInstance(holder.getProject()));
isIterable = resolved.stream().anyMatch(r -> InterfacesExtractUtil.getCrawlInheritanceTree(r, true).stream().anyMatch(c -> c.getFQN().equals("\\Countable")));
}
return isIterable;
});
}
return false;
}
private boolean isNullableCoreType(@NotNull Set<String> resolvedTypesSet) {
boolean result = false;
if (resolvedTypesSet.size() == 2 && resolvedTypesSet.contains(Types.strNull)) {
result = resolvedTypesSet.contains(Types.strInteger) || resolvedTypesSet.contains(Types.strFloat) || resolvedTypesSet.contains(Types.strBoolean) || resolvedTypesSet.contains(Types.strResource);
}
return result;
}
};
}
Aggregations