blob: 15495d709bfaaccd0a44f556be0db412390fe03d [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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.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.ArrayCreation;
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.Assignment.Type;
import org.netbeans.modules.php.editor.parser.astnodes.Block;
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.FieldAccess;
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.Identifier;
import org.netbeans.modules.php.editor.parser.astnodes.IfStatement;
import org.netbeans.modules.php.editor.parser.astnodes.InfixExpression;
import org.netbeans.modules.php.editor.parser.astnodes.LambdaFunctionDeclaration;
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.StaticFieldAccess;
import org.netbeans.modules.php.editor.parser.astnodes.SwitchCase;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.WhileStatement;
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 ImmutableVariablesHint extends HintRule implements CustomisableRule {
private static final String HINT_ID = "Immutable.Variables.Hint"; //NOI18N
private static final String NUMBER_OF_ALLOWED_ASSIGNMENTS = "php.verification.number.of.allowed.assignments"; //NOI18N
private static final int DEFAULT_NUMBER_OF_ALLOWED_ASSIGNMENTS = 1;
private static final List<String> UNCHECKED_VARIABLES = 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
}
@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, context.doc);
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
hints.addAll(checkVisitor.getHints());
}
private class CheckVisitor extends DefaultVisitor {
private final FileObject fileObject;
private final BaseDocument baseDocument;
private final ArrayDeque<ASTNode> parentNodes = new ArrayDeque<>();
private final Map<ASTNode, Map<String, List<Variable>>> assignments = new HashMap<>();
private final List<Hint> hints = new ArrayList<>();
private boolean variableAssignment;
private final int numberOfAllowedAssignments;
CheckVisitor(FileObject fileObject, BaseDocument baseDocument) {
this.fileObject = fileObject;
this.baseDocument = baseDocument;
this.numberOfAllowedAssignments = getNumberOfAllowedAssignments(preferences);
}
public List<Hint> getHints() {
for (Map<String, List<Variable>> names : assignments.values()) {
checkNamesInScope(names);
}
return Collections.unmodifiableList(hints);
}
private void checkNamesInScope(Map<String, List<Variable>> names) {
for (Entry<String, List<Variable>> entry : names.entrySet()) {
checkAllowedAssignments(entry.getValue());
}
}
private void checkAllowedAssignments(List<Variable> variables) {
int variablesSize = variables.size();
if (variablesSize > numberOfAllowedAssignments) {
createHints(variables);
}
}
@Messages({
"# {0} - Number of allowed assignments",
"# {1} - Number of assignments",
"# {2} - Variable name",
"ImmutableVariablesHintCustom=You should use only:\n{0} assignment(s) ({1} used)\nto a variable:\n${2}\nto avoid accidentally overwriting it and make your code easier to read."
})
private void createHints(List<Variable> variables) {
for (Variable variable : variables) {
createHint(variable, variables.size());
}
}
private void createHint(Variable variable, int numberOfAssignments) {
int start = variable.getStartOffset() + 1;
int end = variable.getEndOffset();
OffsetRange offsetRange = new OffsetRange(start, end);
if (showHint(offsetRange, baseDocument)) {
Identifier variableIdentifier = getIdentifier(variable);
String variableName = variableIdentifier == null ? "?" : variableIdentifier.getName(); //NOI18N
hints.add(new Hint(
ImmutableVariablesHint.this,
Bundle.ImmutableVariablesHintCustom(numberOfAllowedAssignments, numberOfAssignments, variableName),
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(ArrowFunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (parentNodes.peek() instanceof ArrowFunctionDeclaration) {
// nested arrow function
super.visit(node);
} else {
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
}
@Override
public void visit(LambdaFunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(IfStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(CatchClause node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(Block node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (parentNodes.peek() instanceof IfStatement) {
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
} else {
super.visit(node);
}
}
@Override
public void visit(ForStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
scan(node.getInitializers());
scan(node.getConditions());
scan(node.getBody());
parentNodes.pop();
}
@Override
public void visit(ForEachStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(DoStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(WhileStatement node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(SwitchCase node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
parentNodes.push(node);
super.visit(node);
parentNodes.pop();
}
@Override
public void visit(StaticFieldAccess node) {
// intentionally
}
@Override
public void visit(FormalParameter functionParameter) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
Expression parameterName = functionParameter.getParameterName();
if (parameterName instanceof Variable) {
processVariableAssignment((Variable) parameterName);
}
}
@Override
public void visit(Variable node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (variableAssignment) {
processVariableAssignment(node);
}
}
private void processVariableAssignment(Variable node) {
ASTNode parentNode = parentNodes.peek();
Map<String, List<Variable>> names = getNames(parentNode);
Identifier identifier = getIdentifier(node);
if (identifier != null) {
addValidVariable(identifier, names, node);
}
}
private Map<String, List<Variable>> getNames(ASTNode parentNode) {
Map<String, List<Variable>> names = assignments.get(parentNode);
if (names == null) {
names = new HashMap<>();
assignments.put(parentNode, names);
}
return names;
}
private void addValidVariable(Identifier identifier, Map<String, List<Variable>> names, Variable node) {
String name = identifier.getName();
if (!UNCHECKED_VARIABLES.contains(name)) {
List<Variable> variables = getVariables(names, name);
variables.add(node);
}
}
private List<Variable> getVariables(Map<String, List<Variable>> names, String name) {
List<Variable> variables = names.get(name);
if (variables == null) {
variables = new ArrayList<>();
names.put(name, variables);
}
return variables;
}
@Override
public void visit(Assignment node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (node.getOperator().equals(Type.EQUAL)) {
if (parentNodes.peek() instanceof IfStatement) {
parentNodes.push(node);
processEqualAssignment(node);
parentNodes.pop();
} else {
processEqualAssignment(node);
}
}
}
private void processEqualAssignment(Assignment node) {
if (!(node.getRightHandSide() instanceof InfixExpression)
|| (node.getRightHandSide() instanceof InfixExpression && !containsConcatOperator((InfixExpression) node.getRightHandSide()))) {
variableAssignment = true;
scan(node.getLeftHandSide());
variableAssignment = false;
}
}
private boolean containsConcatOperator(InfixExpression infixExpression) {
boolean retval = false;
if (infixExpression.getOperator().equals(InfixExpression.OperatorType.CONCAT)) {
retval = true;
} else if (infixExpression.getLeft() instanceof InfixExpression) {
retval = containsConcatOperator((InfixExpression) infixExpression.getLeft());
} else if (infixExpression.getLeft() instanceof InfixExpression) {
retval = containsConcatOperator((InfixExpression) infixExpression.getRight());
}
return retval;
}
@Override
public void visit(ArrayAccess node) {
// intentionally
}
@Override
public void visit(ArrayCreation node) {
// intentionally
}
@Override
public void visit(FieldAccess node) {
// intentionally
}
@CheckForNull
private Identifier getIdentifier(Variable variable) {
Identifier retval = null;
if (variable != null && variable.isDollared()) {
retval = separateIdentifier(variable);
}
return retval;
}
@CheckForNull
private Identifier separateIdentifier(Variable variable) {
Identifier retval = null;
if (variable.getName() instanceof Identifier) {
retval = (Identifier) variable.getName();
}
return retval;
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@Messages("ImmutableVariableHintDesc=Checks a number of assignments into a variable in a block.")
public String getDescription() {
return Bundle.ImmutableVariableHintDesc();
}
@Override
@Messages("ImmutableVariableHintDispName=Immutable Variables")
public String getDisplayName() {
return Bundle.ImmutableVariableHintDispName();
}
@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 ImmutableVariablesCustomizer(preferences, this);
setNumberOfAllowedAssignments(preferences, getNumberOfAllowedAssignments(preferences));
return customizer;
}
public void setNumberOfAllowedAssignments(Preferences preferences, Integer value) {
preferences.putInt(NUMBER_OF_ALLOWED_ASSIGNMENTS, value);
}
public int getNumberOfAllowedAssignments(Preferences preferences) {
return preferences.getInt(NUMBER_OF_ALLOWED_ASSIGNMENTS, DEFAULT_NUMBER_OF_ALLOWED_ASSIGNMENTS);
}
}