use of com.intellij.psi.PsiElementVisitor in project phpinspectionsea by kalessil.
the class IssetArgumentExistenceInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpBinaryExpression(@NotNull BinaryExpression expression) {
final PsiElement candidate = expression.getLeftOperand();
if (candidate instanceof Variable && PhpTokenTypes.opCOALESCE == expression.getOperationType()) {
final Variable variable = (Variable) candidate;
if (!this.getSuppliedVariables(expression).contains(variable.getName())) {
analyzeExistence(variable);
}
}
}
@Override
public void visitPhpEmpty(@NotNull PhpEmpty expression) {
this.analyzeArgumentsExistence(expression.getVariables());
}
@Override
public void visitPhpIsset(@NotNull PhpIsset expression) {
this.analyzeArgumentsExistence(expression.getVariables());
}
private void analyzeArgumentsExistence(@NotNull PhpExpression[] arguments) {
if (arguments.length > 0) {
final Set<String> parameters = this.getSuppliedVariables(arguments[0]);
for (final PhpExpression argument : arguments) {
if (argument instanceof Variable && !parameters.contains(argument.getName())) {
this.analyzeExistence((Variable) argument);
}
}
parameters.clear();
}
}
private void analyzeExistence(@NotNull Variable variable) {
final String variableName = variable.getName();
if (!variableName.isEmpty() && !specialVariables.contains(variableName)) {
final Function scope = ExpressionSemanticUtil.getScope(variable);
final GroupStatement body = scope == null ? null : ExpressionSemanticUtil.getGroupStatement(scope);
if (body != null) {
for (final Variable reference : PsiTreeUtil.findChildrenOfType(body, Variable.class)) {
if (reference.getName().equals(variableName)) {
boolean report = reference == variable;
if (!report) {
final PsiElement parent = reference.getParent();
if (parent instanceof AssignmentExpression) {
report = PsiTreeUtil.findCommonParent(reference, variable) == parent;
}
}
if (report) {
/* variable created dynamically in a loop: hacky stuff, but nevertheless */
PsiElement loopCandidate = reference.getParent();
while (loopCandidate != null && loopCandidate != scope) {
if (OpenapiTypesUtil.isLoop(loopCandidate)) {
report = PsiTreeUtil.findChildrenOfType(loopCandidate, AssignmentExpression.class).stream().noneMatch(assignment -> {
final PsiElement container = assignment.getVariable();
return container instanceof Variable && ((Variable) container).getName().equals(variableName);
});
break;
}
loopCandidate = loopCandidate.getParent();
}
if (report && (IGNORE_INCLUDES || !this.hasIncludes(scope)) && !this.hasGotos(scope)) {
holder.registerProblem(variable, String.format(MessagesPresentationUtil.prefixWithEa(messagePattern), variableName), ProblemHighlightType.GENERIC_ERROR);
}
}
break;
}
}
}
}
}
@NotNull
private Set<String> getSuppliedVariables(@NotNull PsiElement expression) {
final Set<String> result = new HashSet<>();
final Function scope = ExpressionSemanticUtil.getScope(expression);
if (scope != null) {
for (final Parameter parameter : scope.getParameters()) {
result.add(parameter.getName());
}
final List<Variable> used = ExpressionSemanticUtil.getUseListVariables(scope);
if (used != null && !used.isEmpty()) {
used.forEach(v -> result.add(v.getName()));
used.clear();
}
}
return result;
}
private boolean hasIncludes(@NotNull Function function) {
final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(function);
if (body != null) {
return PsiTreeUtil.findChildOfType(body, Include.class) != null;
}
return false;
}
private boolean hasGotos(@NotNull Function function) {
final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(function);
if (body != null) {
return PsiTreeUtil.findChildOfType(body, PhpGoto.class) != null;
}
return false;
}
};
}
use of com.intellij.psi.PsiElementVisitor in project phpinspectionsea by kalessil.
the class UnusedConstructorDependenciesInspector method buildVisitor.
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@NotNull
private Map<String, Field> getPrivateFields(@NotNull PhpClass clazz) {
final Map<String, Field> privateFields = new HashMap<>();
for (final Field field : clazz.getOwnFields()) {
if (!field.isConstant()) {
final PhpModifier modifiers = field.getModifier();
if (modifiers.isPrivate() && !modifiers.isStatic()) {
final PhpDocTag[] tags = PsiTreeUtil.getChildrenOfType(field.getDocComment(), PhpDocTag.class);
final boolean annotated = tags != null && Arrays.stream(tags).anyMatch(t -> !t.getName().equals(t.getName().toLowerCase()));
if (!annotated) {
privateFields.put(field.getName(), field);
}
}
}
}
return privateFields;
}
@NotNull
private Map<String, List<FieldReference>> getFieldReferences(@NotNull Method method, @NotNull Map<String, Field> privateFields) {
final Map<String, List<FieldReference>> filteredReferences = new HashMap<>();
if (!method.isAbstract()) {
final Collection<FieldReference> references = PsiTreeUtil.findChildrenOfType(method, FieldReference.class);
for (final FieldReference ref : references) {
final String fieldName = ref.getName();
if (fieldName != null && privateFields.containsKey(fieldName)) {
final PsiElement resolved = OpenapiResolveUtil.resolveReference(ref);
if (resolved == null || (resolved instanceof Field && privateFields.containsValue(resolved))) {
filteredReferences.computeIfAbsent(fieldName, k -> new ArrayList<>()).add(ref);
}
}
}
references.clear();
}
return filteredReferences;
}
@NotNull
private Map<String, List<FieldReference>> getMethodsFieldReferences(@NotNull PhpClass clazz, @NotNull Method constructor, @NotNull Map<String, Field> privateFields) {
final Map<String, List<FieldReference>> filteredReferences = new HashMap<>();
/* collect methods to inspect, incl. once from traits */
final List<Method> methodsToCheck = new ArrayList<>();
Collections.addAll(methodsToCheck, clazz.getOwnMethods());
for (final PhpClass trait : clazz.getTraits()) {
Collections.addAll(methodsToCheck, trait.getOwnMethods());
}
/* find references */
for (final Method method : methodsToCheck) {
if (method != constructor) {
final Map<String, List<FieldReference>> innerReferences = getFieldReferences(method, privateFields);
if (!innerReferences.isEmpty()) {
/* merge method's scan results into common container */
innerReferences.forEach((fieldName, references) -> {
filteredReferences.computeIfAbsent(fieldName, name -> new ArrayList<>()).addAll(references);
references.clear();
});
}
innerReferences.clear();
}
}
methodsToCheck.clear();
return filteredReferences;
}
@Override
public void visitPhpMethod(@NotNull Method method) {
/* filter classes which needs to be analyzed */
final PhpClass clazz = method.getContainingClass();
if (null == clazz || clazz.isInterface() || clazz.isTrait() || null == clazz.getOwnConstructor() || 0 == clazz.getOwnFields().length || 0 == clazz.getOwnMethods().length) {
return;
}
/* run inspection only in constructors and if own private fields being defined */
final Method constructor = clazz.getOwnConstructor();
if (method != constructor) {
return;
}
final Map<String, Field> clazzPrivateFields = this.getPrivateFields(clazz);
if (clazzPrivateFields.isEmpty()) {
return;
}
/* === intensive part : extract references === */
final Map<String, List<FieldReference>> constructorsReferences = getFieldReferences(constructor, clazzPrivateFields);
if (!constructorsReferences.isEmpty()) {
/* constructor's references being identified */
final Map<String, List<FieldReference>> otherReferences = getMethodsFieldReferences(clazz, constructor, clazzPrivateFields);
/* methods's references being identified, time to re-visit constructor's references */
constructorsReferences.forEach((fieldName, references) -> {
/* field is not used, report in constructor, IDE detects unused fields */
if (!otherReferences.containsKey(fieldName)) {
/* ensure the field reference in constructor is not bound to closures only */
if (references.stream().anyMatch(r -> ExpressionSemanticUtil.getScope(r) == constructor)) {
this.doReport(holder, references);
}
}
references.clear();
});
/* release references found in the methods */
otherReferences.values().forEach(List::clear);
otherReferences.clear();
}
constructorsReferences.clear();
}
private void doReport(@NotNull ProblemsHolder holder, @NotNull List<FieldReference> fields) {
fields.stream().filter(reference -> {
final PsiElement parent = reference.getParent();
if (OpenapiTypesUtil.isAssignment(parent)) {
return reference == ((AssignmentExpression) parent).getVariable();
}
return false;
}).forEach(reference -> holder.registerProblem(reference, MessagesPresentationUtil.prefixWithEa(message)));
}
};
}
use of com.intellij.psi.PsiElementVisitor in project phpinspectionsea by kalessil.
the class LoopWhichDoesNotLoopInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpForeach(@NotNull ForeachStatement loop) {
final boolean isBlade = holder.getFile().getName().endsWith(".blade.php");
if (!isBlade && this.isNotLooping(loop)) {
/* false-positive: return first element from generator, iterable and co */
final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(loop);
final PsiElement last = body == null ? null : ExpressionSemanticUtil.getLastStatement(body);
if (last != null && !OpenapiTypesUtil.isThrowExpression(last)) {
final PsiElement source = loop.getArray();
if (source instanceof PhpTypedElement) {
final PhpType resolved = OpenapiResolveUtil.resolveType((PhpTypedElement) source, holder.getProject());
final boolean isValid = resolved != null && resolved.filterUnknown().getTypes().stream().anyMatch(type -> foreachExceptions.contains(Types.getType(type)));
if (isValid) {
return;
}
}
}
holder.registerProblem(loop.getFirstChild(), MessagesPresentationUtil.prefixWithEa(message));
}
}
@Override
public void visitPhpFor(@NotNull For loop) {
final boolean isBlade = holder.getFile().getName().endsWith(".blade.php");
if (!isBlade && this.isNotLooping(loop)) {
holder.registerProblem(loop.getFirstChild(), MessagesPresentationUtil.prefixWithEa(message));
}
}
@Override
public void visitPhpWhile(@NotNull While loop) {
final boolean isBlade = holder.getFile().getName().endsWith(".blade.php");
if (!isBlade && this.isNotLooping(loop)) {
holder.registerProblem(loop.getFirstChild(), MessagesPresentationUtil.prefixWithEa(message));
}
}
@Override
public void visitPhpDoWhile(@NotNull DoWhile loop) {
final boolean isBlade = holder.getFile().getName().endsWith(".blade.php");
if (!isBlade && this.isNotLooping(loop)) {
holder.registerProblem(loop.getFirstChild(), MessagesPresentationUtil.prefixWithEa(message));
}
}
private boolean isNotLooping(@NotNull PhpPsiElement loop) {
final GroupStatement body = ExpressionSemanticUtil.getGroupStatement(loop);
if (null == body) {
return false;
}
final PsiElement lastExpression = ExpressionSemanticUtil.getLastStatement(body);
final boolean isLoopTerminatedWithLastExpression = lastExpression instanceof PhpBreak || lastExpression instanceof PhpReturn || OpenapiTypesUtil.isThrowExpression(lastExpression);
/* loop is empty or terminates on first iteration */
if (null != lastExpression && !isLoopTerminatedWithLastExpression) {
return false;
}
/* detect continue statements, which makes the loop looping */
for (final PhpContinue expression : PsiTreeUtil.findChildrenOfType(body, PhpContinue.class)) {
int nestingLevel = 0;
PsiElement parent = expression.getParent();
while (null != parent && !(parent instanceof Function) && !(parent instanceof PsiFile)) {
if (OpenapiTypesUtil.isLoop(parent)) {
++nestingLevel;
if (parent == loop) {
/* extract level of continuation from the statement */
int continueLevel = 1;
final PsiElement argument = expression.getArgument();
if (null != argument) {
try {
continueLevel = Integer.parseInt(argument.getText());
} catch (final NumberFormatException notParsed) {
continueLevel = 1;
}
}
/* matched continue for the current loop */
if (continueLevel == nestingLevel) {
return false;
}
}
}
parent = parent.getParent();
}
}
return true;
}
};
}
use of com.intellij.psi.PsiElementVisitor 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.intellij.psi.PsiElementVisitor in project phpinspectionsea by kalessil.
the class AutoloadingIssuesInspector method buildVisitor.
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new BasePhpElementVisitor() {
@Override
public void visitPhpFile(@NotNull PhpFile file) {
final String fileName = file.getName();
if (fileName.endsWith(".php") && !ignoredFiles.contains(fileName) && !laravelMigration.matcher(fileName).matches()) {
final List<PhpClass> classes = new ArrayList<>();
file.getTopLevelDefs().values().stream().filter(definition -> definition instanceof PhpClass).forEach(definition -> classes.add((PhpClass) definition));
if (classes.size() == 1) {
final PhpClass clazz = classes.get(0);
final String className = clazz.getName();
/* PSR-0 classloading (Package_Subpackage_Class) naming */
String extractedClassName = className;
if (clazz.getFQN().lastIndexOf('\\') == 0 && extractedClassName.indexOf('_') != -1) {
extractedClassName = extractedClassName.substring(1 + extractedClassName.lastIndexOf('_'));
}
/* check the file name as per extraction compliant with PSR-0/PSR-4 standards */
final String expectedClassName = fileName.substring(0, fileName.indexOf('.'));
if (this.isBreakingPsrStandard(className, expectedClassName, extractedClassName) && !this.isWordpressStandard(className, fileName)) {
final PsiElement classNameNode = NamedElementUtil.getNameIdentifier(clazz);
if (classNameNode != null) {
holder.registerProblem(classNameNode, MessagesPresentationUtil.prefixWithEa(message));
}
}
}
classes.clear();
}
}
private boolean isBreakingPsrStandard(@NotNull String className, @NotNull String expectedClassName, @NotNull String extractedClassName) {
return !expectedClassName.equals(extractedClassName) && !expectedClassName.equals(className);
}
private boolean isWordpressStandard(@NotNull String className, @NotNull String fileName) {
final String wordpressFileName = String.format("class-%s.php", className.toLowerCase().replaceAll("_", "-"));
return fileName.endsWith(wordpressFileName);
}
};
}
Aggregations