blob: 2a4e05e8ea9fa68a8c7acf53421b91c87136de61 [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.Collection;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.EditList;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.HintFix;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.editor.api.PhpModifiers;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.model.ClassConstantElement;
import org.netbeans.modules.php.editor.model.ClassScope;
import org.netbeans.modules.php.editor.model.FieldElement;
import org.netbeans.modules.php.editor.model.FileScope;
import org.netbeans.modules.php.editor.model.InterfaceScope;
import org.netbeans.modules.php.editor.model.MethodScope;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle.Messages;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class ModifiersCheckHintError extends HintErrorRule {
private List<Hint> hints;
private FileObject fileObject;
private BaseDocument doc;
private boolean currectClassHasAbstractMethod = false;
@Override
public void invoke(PHPRuleContext context, List<Hint> hints) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() == null) {
return;
}
this.hints = hints;
this.doc = context.doc;
FileScope fileScope = context.fileScope;
fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
if (fileScope != null && fileObject != null) {
Collection<? extends ClassScope> declaredClasses = ModelUtils.getDeclaredClasses(fileScope);
for (ClassScope classScope : declaredClasses) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processClassScope(classScope);
}
Collection<? extends InterfaceScope> declaredInterfaces = ModelUtils.getDeclaredInterfaces(fileScope);
for (InterfaceScope interfaceScope : declaredInterfaces) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processInterfaceScope(interfaceScope);
}
}
}
@Override
@Messages("ModifiersCheckHintDispName=Modifiers Checker")
public String getDisplayName() {
return Bundle.ModifiersCheckHintDispName();
}
private void processClassScope(ClassScope classScope) {
Collection<? extends FieldElement> declaredFields = classScope.getDeclaredFields();
for (FieldElement fieldElement : declaredFields) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processFieldElement(fieldElement);
}
Collection<? extends MethodScope> declaredMethods = classScope.getDeclaredMethods();
for (MethodScope methodScope : declaredMethods) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processMethodScope(methodScope);
}
if (currectClassHasAbstractMethod) {
processPossibleAbstractClass(classScope);
}
currectClassHasAbstractMethod = false;
}
@Messages({
"# {0} - Field name",
"# {1} - Modifier name",
"InvalidField=Field \"{0}\" can not be declared {1}"
})
private void processFieldElement(FieldElement fieldElement) {
PhpModifiers phpModifiers = fieldElement.getPhpModifiers();
List<HintFix> fixes;
String invalidModifier;
if (phpModifiers.isAbstract()) {
invalidModifier = "abstract"; //NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, fieldElement.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidField(fieldElement.getName(), invalidModifier), fieldElement.getNameRange(), fixes));
} else if (phpModifiers.isFinal()) {
invalidModifier = "final"; //NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, fieldElement.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidField(fieldElement.getName(), invalidModifier), fieldElement.getNameRange(), fixes));
}
}
@Messages({
"# {0} - Method name",
"AbstractFinalMethod=Method \"{0}\" can not be declared abstract and final",
"# {0} - Method name",
"AbstractWithBlockMethod=Abstract method \"{0}\" can not contain body",
"# {0} - Method name",
"AbstractPrivateMethod=Abstract method \"{0}\" can not be declared private"
})
private void processMethodScope(MethodScope methodScope) {
PhpModifiers phpModifiers = methodScope.getPhpModifiers();
List<HintFix> fixes;
if (phpModifiers.isAbstract() && phpModifiers.isFinal()) {
fixes = new ArrayList<>();
fixes.add(new RemoveModifierFix(doc, "abstract", methodScope.getOffset())); //NOI18N
fixes.add(new RemoveModifierFix(doc, "final", methodScope.getOffset())); //NOI18N
hints.add(new SimpleHint(Bundle.AbstractFinalMethod(methodScope.getName()), methodScope.getNameRange(), fixes));
} else if (phpModifiers.isAbstract() && methodScope.getBlockRange() != null) {
fixes = Collections.<HintFix>singletonList(new RemoveBodyFix(doc, methodScope));
hints.add(new SimpleHint(Bundle.AbstractWithBlockMethod(methodScope.getName()), methodScope.getNameRange(), fixes));
} else if (phpModifiers.isAbstract() && phpModifiers.isPrivate()) {
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, "private", methodScope.getOffset())); //NOI18N
hints.add(new SimpleHint(Bundle.AbstractPrivateMethod(methodScope.getName()), methodScope.getNameRange(), fixes));
}
if (phpModifiers.isAbstract()) {
currectClassHasAbstractMethod = true;
}
}
private void processInterfaceScope(InterfaceScope interfaceScope) {
Collection<? extends MethodScope> declaredMethods = interfaceScope.getDeclaredMethods();
for (MethodScope methodScope : declaredMethods) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processInterfaceMethodScope(methodScope);
}
// PHP7.1
Collection<? extends ClassConstantElement> declaredConstants = interfaceScope.getDeclaredConstants();
for (ClassConstantElement declaredConstant : declaredConstants) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
processInterfaceClassConstant(declaredConstant);
}
}
@Messages({
"# {0} - Method name",
"# {1} - Modifier name",
"InvalidIfaceMethod=Interface method \"{0}\" can not be declared {1}",
"# {0} - Method name",
"IfaceMethodWithBlock=Interface method \"{0}\" can not contain body"
})
private void processInterfaceMethodScope(MethodScope methodScope) {
PhpModifiers phpModifiers = methodScope.getPhpModifiers();
List<HintFix> fixes;
String invalidModifier;
if (phpModifiers.isPrivate()) {
invalidModifier = "private"; //NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, methodScope.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidIfaceMethod(methodScope.getName(), invalidModifier), methodScope.getNameRange(), fixes));
} else if (phpModifiers.isProtected()) {
invalidModifier = "protected"; //NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, methodScope.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidIfaceMethod(methodScope.getName(), invalidModifier), methodScope.getNameRange(), fixes));
} else if (phpModifiers.isFinal()) {
invalidModifier = "final"; //NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, methodScope.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidIfaceMethod(methodScope.getName(), invalidModifier), methodScope.getNameRange(), fixes));
} else if (methodScope.getBlockRange() != null && methodScope.getBlockRange().getLength() != 1) {
fixes = Collections.<HintFix>singletonList(new RemoveBodyFix(doc, methodScope));
hints.add(new SimpleHint(Bundle.IfaceMethodWithBlock(methodScope.getName()), methodScope.getNameRange(), fixes));
}
}
@Messages({
"# {0} - Constant name",
"# {1} - Modifier name",
"InvalidIfaceConstant=Interface const \"{0}\" can not be declared {1}",
})
private void processInterfaceClassConstant(ClassConstantElement classConstant) {
PhpModifiers phpModifiers = classConstant.getPhpModifiers();
List<HintFix> fixes;
String invalidModifier;
if (phpModifiers.isPrivate()) {
invalidModifier = "private"; // NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, classConstant.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidIfaceConstant(classConstant.getName(), invalidModifier), classConstant.getNameRange(), fixes));
} else if (phpModifiers.isProtected()) {
invalidModifier = "protected"; // NOI18N
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, invalidModifier, classConstant.getOffset()));
hints.add(new SimpleHint(Bundle.InvalidIfaceConstant(classConstant.getName(), invalidModifier), classConstant.getNameRange(), fixes));
}
}
@Messages({
"# {0} - Class name",
"PossibleAbstractClass=Class \"{0}\" contains abstract methods and must be declared abstract",
"# {0} - Class name",
"FinalPossibleAbstractClass=Class \"{0}\" contains abstract methods and can not be declared final"
})
private void processPossibleAbstractClass(ClassScope classScope) {
List<HintFix> fixes;
if (!classScope.isAbstract()) {
fixes = Collections.<HintFix>singletonList(new AddModifierFix(doc, "abstract", classScope.getOffset())); //NOI18N
hints.add(new SimpleHint(Bundle.PossibleAbstractClass(classScope.getName()), classScope.getNameRange(), fixes));
}
if (classScope.isFinal()) {
fixes = Collections.<HintFix>singletonList(new RemoveModifierFix(doc, "final", classScope.getOffset())); //NOI18N
hints.add(new SimpleHint(Bundle.FinalPossibleAbstractClass(classScope.getName()), classScope.getNameRange(), fixes));
}
}
private class SimpleHint extends Hint {
public SimpleHint(String description, OffsetRange range, List<HintFix> fixes) {
super(ModifiersCheckHintError.this, description, fileObject, range, fixes, 500);
}
public SimpleHint(String description, OffsetRange range) {
this(description, range, null);
}
}
private abstract class AbstractHintFix implements HintFix {
protected final BaseDocument doc;
public AbstractHintFix(BaseDocument doc) {
this.doc = doc;
}
@Override
public boolean isSafe() {
return true;
}
@Override
public boolean isInteractive() {
return false;
}
}
private class RemoveBodyFix extends AbstractHintFix {
private final MethodScope methodScope;
public RemoveBodyFix(BaseDocument doc, MethodScope methodScope) {
super(doc);
this.methodScope = methodScope;
}
@Override
@Messages({
"# {0} - Method name",
"RemoveBodyFixDesc=Remove body of the method: {0}"
})
public String getDescription() {
return Bundle.RemoveBodyFixDesc(methodScope.getName());
}
@Override
public void implement() throws Exception {
EditList edits = new EditList(doc);
edits.replace(methodScope.getBlockRange().getStart(), methodScope.getBlockRange().getLength(), ";", true, 0); //NOI18N
edits.apply();
}
}
private class RemoveModifierFix extends AbstractHintFix {
private final String modifier;
private final int elementOffset;
public RemoveModifierFix(BaseDocument doc, String modifier, int elementOffset) {
super(doc);
this.modifier = modifier;
this.elementOffset = elementOffset;
}
@Override
@Messages({
"# {0} - Modifier name",
"RemoveModifierFixDesc=Remove modifier: {0}"
})
public String getDescription() {
return Bundle.RemoveModifierFixDesc(modifier);
}
@Override
public void implement() throws Exception {
EditList edits = new EditList(doc);
int startOffset = getStartOffset(doc, elementOffset);
int length = elementOffset - startOffset;
String replaceText = doc.getText(startOffset, length).replace(modifier, "").replaceAll("^\\s+", ""); //NOI18N
edits.replace(startOffset, length, replaceText, true, 0);
edits.apply();
}
}
private class AddModifierFix extends AbstractHintFix {
private final String modifier;
private final int elementOffset;
public AddModifierFix(BaseDocument doc, String modifier, int elementOffset) {
super(doc);
this.modifier = modifier;
this.elementOffset = elementOffset;
}
@Override
@Messages({
"# {0} - Modifier name",
"AddModifierFixDesc=Add modifier: {0}"
})
public String getDescription() {
return Bundle.AddModifierFixDesc(modifier);
}
@Override
public void implement() throws Exception {
EditList edits = new EditList(doc);
int startOffset = getStartOffset(doc, elementOffset);
int length = elementOffset - startOffset;
String replaceText = modifier + " " + doc.getText(startOffset, length); //NOI18N
edits.replace(startOffset, length, replaceText, true, 0);
edits.apply();
}
}
private static int getStartOffset(final BaseDocument doc, final int elementOffset) {
int retval = 0;
doc.readLock();
try {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, elementOffset);
if (ts != null) {
ts.move(elementOffset);
TokenId lastTokenId = null;
while (ts.movePrevious()) {
Token t = ts.token();
if (t.id() != PHPTokenId.PHP_PUBLIC && t.id() != PHPTokenId.PHP_PROTECTED && t.id() != PHPTokenId.PHP_PRIVATE
&& t.id() != PHPTokenId.PHP_STATIC && t.id() != PHPTokenId.PHP_FINAL && t.id() != PHPTokenId.PHP_ABSTRACT
&& t.id() != PHPTokenId.PHP_FUNCTION && t.id() != PHPTokenId.WHITESPACE && t.id() != PHPTokenId.PHP_CLASS
&& t.id() != PHPTokenId.PHP_CONST) {
ts.moveNext();
if (lastTokenId == PHPTokenId.WHITESPACE) {
ts.moveNext();
}
retval = ts.offset();
break;
}
lastTokenId = t.id();
}
}
} finally {
doc.readUnlock();
}
return retval;
}
}