blob: 5facfb39d4666cd019735aa872eebfdfe8416c95 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.php.editor.verification;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.prefs.Preferences;
import javax.swing.JComponent;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.HintSeverity;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.api.ElementQuery.Index;
import org.netbeans.modules.php.editor.api.NameKind;
import org.netbeans.modules.php.editor.api.elements.BaseFunctionElement;
import org.netbeans.modules.php.editor.api.elements.ElementFilter;
import org.netbeans.modules.php.editor.api.elements.ParameterElement;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.TypeScope;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
import org.netbeans.modules.php.editor.parser.astnodes.ArrayAccess;
import org.netbeans.modules.php.editor.parser.astnodes.ArrayElement;
import org.netbeans.modules.php.editor.parser.astnodes.ArrowFunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Assignment;
import org.netbeans.modules.php.editor.parser.astnodes.CatchClause;
import org.netbeans.modules.php.editor.parser.astnodes.DoStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Expression;
import org.netbeans.modules.php.editor.parser.astnodes.FieldsDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ForEachStatement;
import org.netbeans.modules.php.editor.parser.astnodes.ForStatement;
import org.netbeans.modules.php.editor.parser.astnodes.FormalParameter;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.GlobalStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Identifier;
import org.netbeans.modules.php.editor.parser.astnodes.LambdaFunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ListVariable;
import org.netbeans.modules.php.editor.parser.astnodes.MethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.NamespaceDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Program;
import org.netbeans.modules.php.editor.parser.astnodes.Reference;
import org.netbeans.modules.php.editor.parser.astnodes.StaticFieldAccess;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.VariableBase;
import org.netbeans.modules.php.editor.parser.astnodes.Variadic;
import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle.Messages;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class UninitializedVariableHint extends HintRule implements CustomisableRule {
private static final String HINT_ID = "Uninitialized.Variable.Hint"; //NOI18N
private static final String CHECK_VARIABLES_INITIALIZED_BY_REFERENCE = "php.verification.check.variables.initialized.by.reference"; //NOI18N
private static final List<String> UNCHECKED_VARIABLES = new ArrayList<>();
private static final List<UglyElement> UGLY_ELEMENTS = new ArrayList<>();
private Preferences preferences;
static {
UNCHECKED_VARIABLES.add("this"); //NOI18N
UNCHECKED_VARIABLES.add("GLOBALS"); //NOI18N
UNCHECKED_VARIABLES.add("_SERVER"); //NOI18N
UNCHECKED_VARIABLES.add("_GET"); //NOI18N
UNCHECKED_VARIABLES.add("_POST"); //NOI18N
UNCHECKED_VARIABLES.add("_FILES"); //NOI18N
UNCHECKED_VARIABLES.add("_COOKIE"); //NOI18N
UNCHECKED_VARIABLES.add("_SESSION"); //NOI18N
UNCHECKED_VARIABLES.add("_REQUEST"); //NOI18N
UNCHECKED_VARIABLES.add("_ENV"); //NOI18N
UNCHECKED_VARIABLES.add("argc"); //NOI18N
UNCHECKED_VARIABLES.add("argv"); //NOI18N
UNCHECKED_VARIABLES.add("HTTP_RAW_POST_DATA"); //NOI18N
UNCHECKED_VARIABLES.add("php_errormsg"); //NOI18N
UNCHECKED_VARIABLES.add("http_response_header"); //NOI18N
UGLY_ELEMENTS.add(new UglyElementImpl("bind_result", "mysqli_stmt"));
}
@Override
public void invoke(PHPRuleContext context, List<Hint> hints) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() == null) {
return;
}
FileObject fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
if (fileObject == null) {
return;
}
if (CancelSupport.getDefault().isCancelled()) {
return;
}
CheckVisitor checkVisitor = new CheckVisitor(fileObject, phpParseResult.getModel(), context.doc);
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
hints.addAll(checkVisitor.getHints());
}
private final class CheckVisitor extends DefaultVisitor {
private final FileObject fileObject;
private final ArrayDeque<ASTNode> parentNodes = new ArrayDeque<>();
private final Map<ASTNode, List<Variable>> initializedVariablesAll = new HashMap<>();
private final Map<ASTNode, List<Variable>> uninitializedVariablesAll = new HashMap<>();
private final List<Hint> hints = new ArrayList<>();
private final Model model;
private final Map<String, Set<BaseFunctionElement>> invocationCache = new HashMap<>();
private final BaseDocument baseDocument;
private final Map<ArrowFunctionDeclaration, Set<String>> arrowFunctionParameters = new HashMap<>();
private ArrowFunctionDeclaration firstArrowFunction = null;
private ArrowFunctionDeclaration currentArrowFunction = null;
private CheckVisitor(FileObject fileObject, Model model, BaseDocument baseDocument) {
this.fileObject = fileObject;
this.model = model;
this.baseDocument = baseDocument;
}
private Collection<? extends Hint> getHints() {
for (ASTNode scopeNode : uninitializedVariablesAll.keySet()) {
createHints(getUninitializedVariables(scopeNode));
}
return Collections.unmodifiableCollection(hints);
}
private void createHints(List<Variable> uninitializedVariables) {
for (Variable variable : uninitializedVariables) {
createHint(variable);
}
}
@Messages({
"# {0} - Name of the variable",
"UninitializedVariableVariableHintCustom=Variable ${0} seems to be uninitialized"
})
private void createHint(Variable variable) {
int start = variable.getStartOffset() + 1;
int end = variable.getEndOffset();
OffsetRange offsetRange = new OffsetRange(start, end);
if (showHint(offsetRange, baseDocument)) {
hints.add(new Hint(UninitializedVariableHint.this, Bundle.UninitializedVariableVariableHintCustom(getVariableName(variable)), fileObject, offsetRange, null, 500));
}
}
@Override
public void visit(Program node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(NamespaceDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(FunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(LambdaFunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
scan(node.getLexicalVariables());
parentNodes.push(node);
initializeExpressions(node.getLexicalVariables());
scan(node.getFormalParameters());
scan(node.getBody());
parentNodes.pop();
}
@Override
public void visit(ArrowFunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (firstArrowFunction == null) {
firstArrowFunction = node;
}
if (currentArrowFunction == null || parentNodes.peek() instanceof LambdaFunctionDeclaration) {
currentArrowFunction = node;
}
Set<String> arrowFunctionParams = getArrowFunctionParams(currentArrowFunction);
for (FormalParameter parameter : node.getFormalParameters()) {
arrowFunctionParams.add(CodeUtils.extractFormalParameterName(parameter));
}
scan(node.getExpression());
// clear
if (firstArrowFunction == node) {
firstArrowFunction = null;
currentArrowFunction = null;
arrowFunctionParameters.clear();
}
}
@Override
public void visit(Assignment node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
VariableBase leftHandSide = node.getLeftHandSide();
initializeVariableBase(leftHandSide);
scan(node.getRightHandSide());
}
@Override
public void visit(CatchClause node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
initializeVariable(node.getVariable());
scan(node.getClassNames());
scan(node.getBody());
}
@Override
public void visit(DoStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
scan(node.getBody());
scan(node.getCondition());
}
@Override
public void visit(ForEachStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
scan(node.getExpression());
initializeExpression(node.getKey());
initializeExpression(node.getValue());
scan(node.getStatement());
}
@Override
public void visit(ForStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
scan(node.getInitializers());
scan(node.getConditions());
scan(node.getBody());
scan(node.getUpdaters());
}
@Override
public void visit(FormalParameter node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
Expression expression = node.getParameterName();
if (expression instanceof Reference) {
Reference reference = (Reference) expression;
expression = reference.getExpression();
} else if (expression instanceof Variadic) {
Variadic variadic = (Variadic) expression;
expression = variadic.getExpression();
}
initializeExpression(expression);
}
@Override
public void visit(GlobalStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
for (Variable variable : node.getVariables()) {
initializeVariable(variable);
}
}
@Override
public void visit(Variable node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (isProcessableVariable(node) && !isArrowFunctionParameter(node)) {
addUninitializedVariable(node);
}
}
@Override
public void visit(FunctionInvocation node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (checkVariablesInitializedByReference(preferences)) {
List<Expression> invocationParametersExp = node.getParameters();
String functionName = CodeUtils.extractFunctionName(node);
if (functionName != null) {
Set<BaseFunctionElement> allFunctions = invocationCache.get(functionName);
if (allFunctions == null) {
allFunctions = new HashSet<>(model.getIndexScope().getIndex().getFunctions(NameKind.create(functionName, QuerySupport.Kind.EXACT)));
invocationCache.put(functionName, allFunctions);
}
processAllFunctions(allFunctions, invocationParametersExp);
}
scan(node.getFunctionName());
} else {
super.visit(node);
}
}
@Override
public void visit(MethodInvocation node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (checkVariablesInitializedByReference(preferences)) {
List<Expression> invocationParametersExp = node.getMethod().getParameters();
if (invocationParametersExp.size() > 0) {
String functionName = CodeUtils.extractFunctionName(node.getMethod());
if (functionName != null) {
Set<BaseFunctionElement> allFunctions = invocationCache.get(functionName);
if (allFunctions == null) {
Collection<? extends TypeScope> resolvedTypes = ModelUtils.resolveType(model, node);
if (resolvedTypes.size() > 0) {
TypeScope resolvedType = ModelUtils.getFirst(resolvedTypes);
Index index = model.getIndexScope().getIndex();
allFunctions = new HashSet<>(ElementFilter.forName(NameKind.exact(functionName)).filter(index.getAllMethods(resolvedType)));
invocationCache.put(functionName, allFunctions);
}
}
processAllFunctions(allFunctions, invocationParametersExp);
}
}
} else {
super.visit(node);
}
}
@Override
public void visit(FieldsDeclaration node) {
// intentionally - variables in fields shouldn't be checked
}
@Override
public void visit(StaticFieldAccess node) {
// intentionally - variables in fields shouldn't be checked
}
private void processAllFunctions(Set<BaseFunctionElement> allFunctions, List<Expression> invocationParametersExp) {
if (allFunctions != null && !allFunctions.isEmpty()) {
BaseFunctionElement methodElement = ModelUtils.getFirst(allFunctions);
if (methodElement != null && !UGLY_ELEMENTS.contains(new UglyElementImpl(methodElement))) {
List<ParameterElement> methodParameters = methodElement.getParameters();
int invocationParamsSize = invocationParametersExp.size();
if (matchNumberOfParams(invocationParamsSize, methodParameters)) {
for (int i = 0; i < invocationParamsSize; i++) {
Expression invocationParameterExp = invocationParametersExp.get(i);
if (methodParameters.get(i).isReference()) {
initializeExpression(invocationParameterExp);
} else {
scan(invocationParameterExp);
}
}
} else {
scan(invocationParametersExp);
}
}
} else {
scan(invocationParametersExp);
}
}
private boolean matchNumberOfParams(int invocationParamsNumber, List<ParameterElement> methodParameters) {
int mandatoryParams = 0;
for (ParameterElement parameterElement : methodParameters) {
if (parameterElement.isMandatory()) {
mandatoryParams++;
}
}
return invocationParamsNumber >= mandatoryParams && invocationParamsNumber <= methodParameters.size();
}
private boolean isProcessableVariable(Variable node) {
Identifier identifier = getIdentifier(node);
return !isInGlobalContext() && identifier != null && !UNCHECKED_VARIABLES.contains(identifier.getName())
&& !isInitialized(node) && !isUninitialized(node);
}
private boolean isInGlobalContext() {
return (parentNodes.peek() instanceof Program) || (parentNodes.peek() instanceof NamespaceDeclaration);
}
private void initializeVariable(Variable variable) {
if (!isInitialized(variable) && !isUninitialized(variable)) {
addInitializedVariable(variable);
}
}
private boolean isInitialized(Variable node) {
return contains(getInitializedVariables(parentNodes.peek()), node);
}
private boolean isUninitialized(Variable node) {
return contains(getUninitializedVariables(parentNodes.peek()), node);
}
private boolean contains(List<Variable> scopeVariables, Variable node) {
boolean retval = false;
String currentVariableName = getVariableName(node);
for (Variable variable : scopeVariables) {
if (currentVariableName.equals(getVariableName(variable))) {
retval = true;
break;
}
}
return retval;
}
private String getVariableName(Variable variable) {
String retval = "";
Identifier identifier = getIdentifier(variable);
if (identifier != null) {
retval = identifier.getName();
}
return retval;
}
private void initializeExpressions(List<Expression> expressions) {
for (Expression expression : expressions) {
initializeExpression(expression);
}
}
private void initializeExpression(Expression expression) {
if (expression instanceof Variable) {
initializeVariable((Variable) expression);
} else if (expression instanceof Reference) {
initializeReference((Reference) expression);
} else if (expression instanceof Variadic) {
initializeVariadic((Variadic) expression);
} else if (expression instanceof ListVariable) { // #249508
initializeListVariable((ListVariable) expression);
}
}
private void initializeReference(Reference node) {
initializeExpression(node.getExpression());
}
private void initializeVariadic(Variadic node) {
initializeExpression(node.getExpression());
}
private void initializeVariableBase(VariableBase variableBase) {
if (variableBase instanceof ArrayAccess) {
initializeArrayAccessVariable((ArrayAccess) variableBase);
} else if (variableBase instanceof Variable) {
initializeVariable((Variable) variableBase);
} else if (variableBase instanceof ListVariable) {
initializeListVariable((ListVariable) variableBase);
} else {
super.visit(variableBase);
}
}
private void initializeArrayAccessVariable(ArrayAccess node) {
VariableBase name = node.getName();
if (name instanceof Variable) {
initializeVariable((Variable) name);
}
}
private void initializeListVariable(ListVariable node) {
List<ArrayElement> elements = node.getElements();
for (ArrayElement element : elements) {
Expression value = element.getValue();
if (value instanceof VariableBase) {
initializeVariableBase((VariableBase)value);
}
}
}
private void addInitializedVariable(Variable node) {
List<Variable> scopeVariables = getInitializedVariables(parentNodes.peek());
scopeVariables.add(node);
}
private void addUninitializedVariable(Variable node) {
List<Variable> scopeVariables = getUninitializedVariables(parentNodes.peek());
scopeVariables.add(node);
}
private List<Variable> getInitializedVariables(ASTNode parent) {
List<Variable> scopeVariables = initializedVariablesAll.get(parent);
if (scopeVariables == null) {
scopeVariables = new ArrayList<>();
initializedVariablesAll.put(parent, scopeVariables);
}
return scopeVariables;
}
private List<Variable> getUninitializedVariables(ASTNode parent) {
List<Variable> scopeVariables = uninitializedVariablesAll.get(parent);
if (scopeVariables == null) {
scopeVariables = new ArrayList<>();
uninitializedVariablesAll.put(parent, scopeVariables);
}
return scopeVariables;
}
@CheckForNull
private Identifier getIdentifier(Variable variable) {
Identifier retval = null;
if (variable != null && variable.isDollared()) {
if (variable.getName() instanceof Identifier) {
retval = (Identifier) variable.getName();
}
}
return retval;
}
private boolean isArrowFunctionParameter(Variable node) {
if (currentArrowFunction == null) {
return false;
}
return getArrowFunctionParams(currentArrowFunction).contains(CodeUtils.extractVariableName(node));
}
private Set<String> getArrowFunctionParams(ArrowFunctionDeclaration arrowFunctionDeclaration) {
Set<String> arrowFunctionParams = arrowFunctionParameters.get(arrowFunctionDeclaration);
if (arrowFunctionParams == null) {
arrowFunctionParams = new HashSet<>();
arrowFunctionParameters.put(arrowFunctionDeclaration, arrowFunctionParams);
}
return arrowFunctionParams;
}
}
private interface UglyElement {
boolean matches(BaseFunctionElement functionElement);
}
private static final class UglyElementImpl implements UglyElement {
private final String methodName;
private final String className;
public UglyElementImpl(String methodName, String className) {
assert methodName != null;
assert className != null;
this.methodName = methodName;
this.className = className;
}
public UglyElementImpl(BaseFunctionElement baseFunctionElement) {
this(baseFunctionElement.getName(), baseFunctionElement.getIn() == null ? "" : baseFunctionElement.getIn());
}
@Override
public boolean matches(BaseFunctionElement functionElement) {
return methodName.equals(functionElement.getName()) && className.equals(functionElement.getIn());
}
@Override
public int hashCode() {
int hash = 5;
hash = 53 * hash + Objects.hashCode(this.methodName);
hash = 53 * hash + Objects.hashCode(this.className);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final UglyElementImpl other = (UglyElementImpl) obj;
if (!Objects.equals(this.methodName, other.methodName)) {
return false;
}
return Objects.equals(this.className, other.className);
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@Messages("UninitializedVariableHintDesc=Detects variables which are used, but not initialized.<br><br>Every variable should be initialized before its first use.")
public String getDescription() {
return Bundle.UninitializedVariableHintDesc();
}
@Override
@Messages("UninitializedVariableHintDispName=Uninitialized Variables")
public String getDisplayName() {
return Bundle.UninitializedVariableHintDispName();
}
@Override
public HintSeverity getDefaultSeverity() {
return HintSeverity.WARNING;
}
@Override
public void setPreferences(Preferences preferences) {
this.preferences = preferences;
}
@Override
public JComponent getCustomizer(Preferences preferences) {
JComponent customizer = new UninitializedVariableCustomizer(preferences, this);
setCheckVariablesInitializedByReference(preferences, checkVariablesInitializedByReference(preferences));
return customizer;
}
public void setCheckVariablesInitializedByReference(Preferences preferences, boolean isEnabled) {
preferences.putBoolean(CHECK_VARIABLES_INITIALIZED_BY_REFERENCE, isEnabled);
}
public boolean checkVariablesInitializedByReference(Preferences preferences) {
return preferences.getBoolean(CHECK_VARIABLES_INITIALIZED_BY_REFERENCE, false);
}
}