blob: 4d51dbef1750e71e93d8b480573256d421409d57 [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.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.api.PhpVersion;
import org.netbeans.modules.php.editor.CodeUtils;
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.Block;
import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ConstantDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.EmptyStatement;
import org.netbeans.modules.php.editor.parser.astnodes.FieldAccess;
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.InterfaceDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.MethodDeclaration;
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.SingleFieldDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Statement;
import org.netbeans.modules.php.editor.parser.astnodes.TraitDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.TypeDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.UseStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public abstract class PSR1Hint extends HintRule {
@Override
public void invoke(PHPRuleContext context, List<Hint> result) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() != null) {
FileObject fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
if (fileObject != null) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
CheckVisitor checkVisitor = createVisitor(fileObject, context.doc);
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
result.addAll(checkVisitor.getHints());
}
}
}
abstract CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument);
public static class ConstantDeclarationHint extends PSR1Hint {
private static final String HINT_ID = "PSR1.Hint.Constant"; //NOI18N
private static final Pattern CONSTANT_PATTERN = Pattern.compile("[A-Z0-9]+[A-Z0-9_]*[A-Z0-9]+"); //NOI18N
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new ConstantsVisitor(this, fileObject, baseDocument);
}
private static final class ConstantsVisitor extends CheckVisitor {
public ConstantsVisitor(PSR1Hint psr1hint, FileObject fileObject, BaseDocument baseDocument) {
super(psr1hint, fileObject, baseDocument);
}
@Override
@NbBundle.Messages("PSR1ConstantDeclarationHintText=Class constants MUST be declared in all upper case with underscore separators.")
public void visit(ConstantDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
for (Identifier constantNameNode : node.getNames()) {
String constantName = constantNameNode.getName();
if (constantName != null && !CONSTANT_PATTERN.matcher(constantName).matches()) {
createHint(constantNameNode, Bundle.PSR1ConstantDeclarationHintText());
}
}
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("PSR1ConstantHintDesc=Class constants MUST be declared in all upper case with underscore separators.")
public String getDescription() {
return Bundle.PSR1ConstantHintDesc();
}
@Override
@NbBundle.Messages("PSR1ConstantHintDisp=Class Constant Declaration")
public String getDisplayName() {
return Bundle.PSR1ConstantHintDisp();
}
}
public static class MethodDeclarationHint extends PSR1Hint {
private static final String HINT_ID = "PSR1.Hint.Method"; //NOI18N
private static final String MAGIC_METHODS = "__(construct|destruct|call|callStatic|get|set|isset|unset|sleep|wakeup|toString|invoke|set_state|clone)"; //NOI18N
private static final Pattern METHOD_PATTERN = Pattern.compile("([a-z]|" + MAGIC_METHODS + ")[a-zA-Z0-9]*"); //NOI18N
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new MethodDeclarationVisitor(this, fileObject, baseDocument);
}
private static final class MethodDeclarationVisitor extends CheckVisitor {
public MethodDeclarationVisitor(PSR1Hint psr1hint, FileObject fileObject, BaseDocument baseDocument) {
super(psr1hint, fileObject, baseDocument);
}
@Override
@NbBundle.Messages("PSR1MethodDeclarationHintText=Method names MUST be declared in camelCase().")
public void visit(MethodDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
Identifier functionNameNode = node.getFunction().getFunctionName();
String methodName = functionNameNode.getName();
if (methodName != null && !METHOD_PATTERN.matcher(methodName).matches()) {
createHint(functionNameNode, Bundle.PSR1MethodDeclarationHintText());
}
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("PSR1MethodDeclarationHintDesc=Method names MUST be declared in camelCase().")
public String getDescription() {
return Bundle.PSR1MethodDeclarationHintDesc();
}
@Override
@NbBundle.Messages("PSR1MethodDeclarationHintDisp=Method Declaration")
public String getDisplayName() {
return Bundle.PSR1MethodDeclarationHintDisp();
}
}
public static class TypeDeclarationHint extends PSR1Hint {
private static final String HINT_ID = "PSR1.Hint.Type"; //NOI18N
private FileObject fileObject;
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
assert fileObject != null;
this.fileObject = fileObject;
return new TypeDeclarationVisitor(this, fileObject, baseDocument);
}
protected boolean isPhp52() {
return CodeUtils.isPhpVersion(fileObject, PhpVersion.PHP_5);
}
private static final class TypeDeclarationVisitor extends CheckVisitor {
private static final Pattern PHP52_TYPE_NAME_PATTERN = Pattern.compile("([A-Z][a-zA-Z0-9]*_)+[A-Z][a-zA-Z0-9]+"); //NOI18N
private static final Pattern PHP53_TYPE_NAME_PATTERN = Pattern.compile("[A-Z][a-zA-Z0-9]+"); //NOI18N
private final boolean isPhp52;
private boolean isInNamedNamespaceDeclaration = false;
private boolean isDeclaredType = false;
public TypeDeclarationVisitor(TypeDeclarationHint typeDeclarationHint, FileObject fileObject, BaseDocument baseDocument) {
super(typeDeclarationHint, fileObject, baseDocument);
isPhp52 = typeDeclarationHint.isPhp52();
}
@Override
public void visit(NamespaceDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
isInNamedNamespaceDeclaration = node.getName() != null;
super.visit(node);
}
@Override
public void visit(ClassDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processTypeDeclaration(node);
}
@Override
public void visit(InterfaceDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processTypeDeclaration(node);
}
@Override
public void visit(TraitDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processTypeDeclaration(node);
}
@NbBundle.Messages("PSR1TypeDeclarationMoreTypesHintText=Each type MUST be in a file by itself.")
private void processTypeDeclaration(TypeDeclaration node) {
Identifier typeNameNode = node.getName();
if (isDeclaredType) {
createHint(typeNameNode, Bundle.PSR1TypeDeclarationMoreTypesHintText());
} else {
isDeclaredType = true;
processFirstDeclaration(typeNameNode);
}
}
private void processFirstDeclaration(Identifier typeNameNode) {
if (isPhp52) {
checkPhp52Violations(typeNameNode);
} else {
checkPhp53Violations(typeNameNode);
}
}
@NbBundle.Messages("PSR1TypeDeclaration52HintText=Type names SHOULD use the pseudo-namespacing convention of Vendor_ prefixes on type names.")
private void checkPhp52Violations(Identifier typeNameNode) {
String typeName = typeNameNode.getName();
if (typeName != null && !PHP52_TYPE_NAME_PATTERN.matcher(typeName).matches()) {
createHint(typeNameNode, Bundle.PSR1TypeDeclaration52HintText());
}
}
@NbBundle.Messages({
"PSR1TypeDeclaration53HintText=Type names MUST be declared in StudlyCaps.",
"PSR1TypeDeclaration53NoNsHintText=Each type MUST be in a namespace of at least one level: a top-level vendor name."
})
private void checkPhp53Violations(Identifier typeNameNode) {
String typeName = typeNameNode.getName();
if (!isInNamedNamespaceDeclaration) {
createHint(typeNameNode, Bundle.PSR1TypeDeclaration53NoNsHintText());
} else {
if (typeName != null && !PHP53_TYPE_NAME_PATTERN.matcher(typeName).matches()) {
createHint(typeNameNode, Bundle.PSR1TypeDeclaration53HintText());
}
}
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("PSR1TypeDeclarationHintDesc=Type names MUST be declared in StudlyCaps (Code written for 5.2.x and before SHOULD use the pseudo-namespacing convention of Vendor_ prefixes on type names). Each type is in a file by itself, and is in a namespace of at least one level: a top-level vendor name.")
public String getDescription() {
return Bundle.PSR1TypeDeclarationHintDesc();
}
@Override
@NbBundle.Messages("PSR1TypeDeclarationHintDisp=Type Declaration")
public String getDisplayName() {
return Bundle.PSR1TypeDeclarationHintDisp();
}
}
public static final class PropertyNameHint extends PSR1Hint {
private static final String HINT_ID = "PSR1.Hint.Property"; //NOI18N
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new PropertyNameVisitor(this, fileObject, baseDocument);
}
private static final class PropertyNameVisitor extends CheckVisitor {
private static final Pattern STUDLY_CAPS_PATTERN = Pattern.compile("[A-Z][a-zA-Z0-9]*"); //NOI18N
private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("[a-z]+([A-Z][a-z0-9]*)*"); //NOI18N
private static final Pattern UNDER_SCORE_PATTERN = Pattern.compile("[a-z]+(_[a-z0-9]*)*"); //NOI18N
private final List<Pattern> possiblePatterns = new ArrayList<>();
public PropertyNameVisitor(PSR1Hint psr1hint, FileObject fileObject, BaseDocument baseDocument) {
super(psr1hint, fileObject, baseDocument);
}
@Override
public void visit(FieldAccess node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
checkProperty(node.getField());
super.visit(node);
}
@Override
public void visit(SingleFieldDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
checkProperty(node.getName());
super.visit(node);
}
@NbBundle.Messages(
"PSR1PropertyNameHintText=Property names SHOULD be declared in $StudlyCaps, $camelCase, or $under_score format (consistently in a scope).\n"
+ "Previous property usage was in a different format, or this property name is absolutely wrong."
)
private void checkProperty(Variable node) {
String propertyName = CodeUtils.extractVariableName(node);
if (propertyName != null) {
String normalizedPropertyName = propertyName.startsWith("$") ? propertyName.substring(1) : propertyName;
if (possiblePatterns.isEmpty()) {
fetchPossiblePatterns(normalizedPropertyName);
}
if (!isValidPropertyName(normalizedPropertyName)) {
createHint(node, Bundle.PSR1PropertyNameHintText());
}
}
}
private boolean isValidPropertyName(String propertyName) {
boolean result = true;
if (possiblePatterns.isEmpty()) {
result = false;
} else {
if (possiblePatterns.size() == 1) { // all property names must match
Pattern pattern = possiblePatterns.get(0);
Matcher matcher = pattern.matcher(propertyName);
if (!matcher.matches()) {
result = false;
}
} else {
// more patterns, probably all previous properties were of unspecific name, i.e. "foobarbaz" (it matches camel and under)
List<Pattern> matchingPatterns = new ArrayList<>();
for (Pattern pattern : possiblePatterns) {
Matcher matcher = pattern.matcher(propertyName);
if (matcher.matches()) {
matchingPatterns.add(pattern);
}
}
if (matchingPatterns.isEmpty()) {
result = false;
} else {
possiblePatterns.clear();
possiblePatterns.addAll(matchingPatterns);
}
}
}
return result;
}
private void fetchPossiblePatterns(String propertyName) {
assert possiblePatterns.isEmpty();
Matcher studlyCapsMatcher = STUDLY_CAPS_PATTERN.matcher(propertyName);
if (studlyCapsMatcher.matches()) {
possiblePatterns.add(STUDLY_CAPS_PATTERN);
} else {
Matcher camelCaseMatcher = CAMEL_CASE_PATTERN.matcher(propertyName);
if (camelCaseMatcher.matches()) {
possiblePatterns.add(CAMEL_CASE_PATTERN);
}
Matcher underScoreMatcher = UNDER_SCORE_PATTERN.matcher(propertyName);
if (underScoreMatcher.matches()) {
possiblePatterns.add(UNDER_SCORE_PATTERN);
}
}
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("PSR1PropertyNameHintDesc=Property names SHOULD be declared in $StudlyCaps, $camelCase, or $under_score format (consistently in a scope).")
public String getDescription() {
return Bundle.PSR1PropertyNameHintDesc();
}
@Override
@NbBundle.Messages("PSR1PropertyNameHintDisp=Property Name")
public String getDisplayName() {
return Bundle.PSR1PropertyNameHintDisp();
}
}
public static final class SideEffectHint extends PSR1Hint {
private static final String HINT_ID = "PSR1.Hint.Side.Effect"; //NOI18N
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new SideEffectVisitor(this, fileObject, baseDocument);
}
private static final class SideEffectVisitor extends CheckVisitor {
private boolean containsDeclaration = false;
private ASTNode firstSideEffectNode;
public SideEffectVisitor(PSR1Hint psr1hint, FileObject fileObject, BaseDocument baseDocument) {
super(psr1hint, fileObject, baseDocument);
}
@Override
public void visit(Program node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
checkStatements(node.getStatements());
checkSideEffects();
}
private void checkStatements(List<Statement> statements) {
for (Statement statement : statements) {
checkStatement(statement);
}
}
private void checkStatement(ASTNode node) {
if (isNamespaceDeclaration(node)) {
NamespaceDeclaration namespaceDeclaration = (NamespaceDeclaration) node;
checkStatements(namespaceDeclaration.getBody().getStatements());
} else if (isDeclaration(node)) {
containsDeclaration = true;
} else if (isCondition(node)) {
IfStatement ifStatement = (IfStatement) node;
checkCondition(ifStatement.getTrueStatement());
checkCondition(ifStatement.getFalseStatement());
} else {
if (!isAllowedEverywhere(node)) {
initSideEffect(node);
}
}
}
private void checkCondition(Statement node) {
if (node instanceof Block) {
Block body = (Block) node;
checkStatements(body.getStatements());
} else {
checkStatement(node);
}
}
private void initSideEffect(ASTNode node) {
if (firstSideEffectNode == null) {
firstSideEffectNode = node;
}
}
@NbBundle.Messages(
"PSR1SideEffectHintText=A file SHOULD declare new symbols and cause no other side effects, or it SHOULD execute logic with side effects, "
+ "but SHOULD NOT do both."
)
private void checkSideEffects() {
if (isSideEffect()) {
createHint(firstSideEffectNode, Bundle.PSR1SideEffectHintText());
}
}
private boolean isSideEffect() {
return firstSideEffectNode != null && containsDeclaration;
}
private static boolean isNamespaceDeclaration(ASTNode node) {
return node instanceof NamespaceDeclaration;
}
private static boolean isCondition(ASTNode node) {
return node instanceof IfStatement;
}
private static boolean isDeclaration(ASTNode node) {
return node instanceof TypeDeclaration || node instanceof FunctionDeclaration || node instanceof ConstantDeclaration;
}
private static boolean isAllowedEverywhere(ASTNode node) {
return node instanceof UseStatement || node instanceof NamespaceDeclaration || node instanceof EmptyStatement;
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages(
"PSR1SideEffectHintDesc=A file SHOULD declare new symbols and cause no other side effects, or it SHOULD execute logic with side effects, "
+ "but SHOULD NOT do both."
)
public String getDescription() {
return Bundle.PSR1SideEffectHintDesc();
}
@Override
@NbBundle.Messages("PSR1SideEffectHintDisp=Side Effects")
public String getDisplayName() {
return Bundle.PSR1SideEffectHintDisp();
}
}
private abstract static class CheckVisitor extends DefaultVisitor {
private final PSR1Hint psr1hint;
private final FileObject fileObject;
private final BaseDocument baseDocument;
private final List<Hint> hints;
public CheckVisitor(PSR1Hint psr1hint, FileObject fileObject, BaseDocument baseDocument) {
this.psr1hint = psr1hint;
this.fileObject = fileObject;
this.baseDocument = baseDocument;
this.hints = new ArrayList<>();
}
public List<Hint> getHints() {
return hints;
}
@NbBundle.Messages({
"# {0} - Text which describes the violation",
"PSR1ViolationHintText=PSR-1 Violation:\n{0}"
})
protected void createHint(ASTNode node, String message) {
OffsetRange offsetRange = new OffsetRange(node.getStartOffset(), node.getEndOffset());
if (psr1hint.showHint(offsetRange, baseDocument)) {
hints.add(new Hint(
psr1hint,
Bundle.PSR1ViolationHintText(message),
fileObject,
offsetRange,
null,
500));
}
}
}
@Override
public boolean getDefaultEnabled() {
return false;
}
}