blob: c5c0e085f916b25817a42c606491b78f25d7bc7c [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.io.IOException;
import java.util.Arrays;
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.Set;
import javax.swing.text.BadLocationException;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.lexer.Token;
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.api.UiUtils;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.api.util.StringUtils;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.completion.PHPCompletionItem;
import org.netbeans.modules.php.editor.completion.PHPCompletionItem.MethodDeclarationItem;
import org.netbeans.modules.php.editor.api.ElementQuery;
import org.netbeans.modules.php.editor.api.NameKind;
import org.netbeans.modules.php.editor.api.PhpElementKind;
import org.netbeans.modules.php.editor.api.QualifiedName;
import org.netbeans.modules.php.editor.api.elements.BaseFunctionElement.PrintAs;
import org.netbeans.modules.php.editor.api.elements.ClassElement;
import org.netbeans.modules.php.editor.api.elements.ElementFilter;
import org.netbeans.modules.php.editor.api.elements.FieldElement;
import org.netbeans.modules.php.editor.api.elements.MethodElement;
import org.netbeans.modules.php.editor.api.elements.TypeConstantElement;
import org.netbeans.modules.php.editor.elements.MethodElementImpl;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.model.ClassScope;
import org.netbeans.modules.php.editor.model.InterfaceScope;
import org.netbeans.modules.php.editor.model.MethodScope;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelElement;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.TraitScope;
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.BodyDeclaration.Modifier;
import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ClassInstanceCreation;
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.MethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.StaticConstantAccess;
import org.netbeans.modules.php.editor.parser.astnodes.StaticFieldAccess;
import org.netbeans.modules.php.editor.parser.astnodes.StaticMethodInvocation;
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.visitors.DefaultTreePathVisitor;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle.Messages;
/**
* @author Radek Matous
*/
public class IntroduceSuggestion extends SuggestionRule {
private static final String UNKNOWN_FILE_NAME = "?"; //NOI18N
private static final String NAMESPACE_PARAMETER_NAME = "namespace"; //NOI18N
private static final String NAMESPACE_SEPARATOR = "\\"; //NOI18N
@Override
public String getId() {
return "Introduce.Hint"; //NOI18N
}
@Override
@Messages("IntroduceHintDesc=Introduce Hint")
public String getDescription() {
return Bundle.IntroduceHintDesc();
}
@Override
@Messages("IntroduceHintDispName=Introduce Hint")
public String getDisplayName() {
return Bundle.IntroduceHintDispName();
}
@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;
}
int caretOffset = getCaretOffset();
final BaseDocument doc = context.doc;
if (CancelSupport.getDefault().isCancelled()) {
return;
}
OffsetRange lineBounds = VerificationUtils.createLineBounds(caretOffset, doc);
if (lineBounds.containsInclusive(caretOffset)) {
final Model model = phpParseResult.getModel();
IntroduceFixVisitor introduceFixVisitor = new IntroduceFixVisitor(model, lineBounds);
phpParseResult.getProgram().accept(introduceFixVisitor);
IntroduceFix variableFix = introduceFixVisitor.getIntroduceFix();
if (variableFix != null) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
hints.add(new Hint(IntroduceSuggestion.this, getDisplayName(),
fileObject, variableFix.getOffsetRange(),
Collections.<HintFix>singletonList(variableFix), 500));
}
}
}
private static class IntroduceFixVisitor extends DefaultTreePathVisitor {
private final Model model;
private final OffsetRange lineBounds;
private IntroduceFix fix;
IntroduceFixVisitor(Model model, OffsetRange lineBounds) {
this.lineBounds = lineBounds;
this.model = model;
}
@Override
public void scan(ASTNode node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (node != null && (VerificationUtils.isBefore(node.getStartOffset(), lineBounds.getEnd()))) {
super.scan(node);
}
}
@Override
public void visit(ClassInstanceCreation instanceCreation) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (!instanceCreation.isAnonymous()
&& lineBounds.containsInclusive(instanceCreation.getStartOffset())) {
String clzName = CodeUtils.extractClassName(instanceCreation.getClassName());
clzName = (clzName != null && clzName.trim().length() > 0) ? clzName : null;
ElementQuery.Index index = model.getIndexScope().getIndex();
Set<ClassElement> classes = Collections.emptySet();
if (StringUtils.hasText(clzName)) {
classes = index.getClasses(NameKind.exact(clzName));
}
if (clzName != null && classes.isEmpty()) {
ClassElement clz = getIndexedClass(clzName);
if (clz == null) {
fix = IntroduceClassFix.getInstance(clzName, model, instanceCreation);
}
}
}
super.visit(instanceCreation);
}
@Override
public void visit(MethodInvocation methodInvocation) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineBounds.containsInclusive(methodInvocation.getStartOffset())) {
String methName = CodeUtils.extractFunctionName(methodInvocation.getMethod());
if (StringUtils.hasText(methName)) {
Collection<? extends TypeScope> allTypes = ModelUtils.resolveType(model, methodInvocation);
if (allTypes.size() == 1) {
TypeScope type = ModelUtils.getFirst(allTypes);
ElementQuery.Index index = model.getIndexScope().getIndex();
Set<MethodElement> allMethods = ElementFilter.forName(NameKind.exact(methName)).filter(index.getAllMethods(type));
if (allMethods.isEmpty()) {
assert type != null;
FileObject fileObject = type.getFileObject();
BaseDocument document = fileObject != null ? GsfUtilities.getDocument(fileObject, true) : null;
if (document != null && fileObject.canWrite()) {
fix = new IntroduceMethodFix(document, methodInvocation, type);
}
}
}
}
}
super.visit(methodInvocation);
}
@Override
public void visit(StaticMethodInvocation methodInvocation) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineBounds.containsInclusive(methodInvocation.getStartOffset())) {
String methName = CodeUtils.extractFunctionName(methodInvocation.getMethod());
String clzName = CodeUtils.extractUnqualifiedClassName(methodInvocation);
if (clzName != null && StringUtils.hasText(methName)) {
Collection<? extends TypeScope> allTypes = ModelUtils.resolveType(model, methodInvocation);
if (allTypes.size() == 1) {
TypeScope type = ModelUtils.getFirst(allTypes);
ElementQuery.Index index = model.getIndexScope().getIndex();
final ElementFilter nameFilter = ElementFilter.forName(NameKind.exact(methName));
final ElementFilter staticFilter = ElementFilter.forStaticModifiers(true);
Set<MethodElement> allMethods = ElementFilter.allOf(nameFilter, staticFilter).filter(index.getAllMethods(type));
if (allMethods.isEmpty()) {
assert type != null;
FileObject fileObject = type.getFileObject();
BaseDocument document = fileObject != null ? GsfUtilities.getDocument(fileObject, true) : null;
if (document != null && fileObject.canWrite()) {
fix = new IntroduceStaticMethodFix(document, methodInvocation, type);
}
}
}
}
}
super.visit(methodInvocation);
}
@Override
public void visit(FieldAccess fieldAccess) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineBounds.containsInclusive(fieldAccess.getStartOffset())) {
String fieldName = CodeUtils.extractVariableName(fieldAccess.getField());
if (StringUtils.hasText(fieldName)) {
Collection<? extends TypeScope> allTypes = ModelUtils.resolveType(model, fieldAccess);
if (allTypes.size() == 1) {
TypeScope type = ModelUtils.getFirst(allTypes);
ElementQuery.Index index = model.getIndexScope().getIndex();
Set<FieldElement> allFields = ElementFilter.forName(NameKind.exact(fieldName)).filter(index.getAlllFields(type));
if (allFields.isEmpty()) {
assert type != null;
FileObject fileObject = type.getFileObject();
BaseDocument document = fileObject != null ? GsfUtilities.getDocument(fileObject, false) : null;
if (document != null && fileObject.canWrite()) {
if (type instanceof ClassScope || type instanceof TraitScope) {
fix = new IntroduceFieldFix(document, fieldAccess, type);
}
}
}
}
}
}
super.visit(fieldAccess);
}
@Override
public void visit(StaticFieldAccess fieldAccess) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineBounds.containsInclusive(fieldAccess.getStartOffset())) {
final Variable field = fieldAccess.getField();
String clzName = CodeUtils.extractUnqualifiedClassName(fieldAccess);
if (clzName != null) {
String fieldName = CodeUtils.extractVariableName(field);
if (!StringUtils.hasText(fieldName)) {
return;
}
assert fieldName != null;
if (fieldName.startsWith("$")) { //NOI18N
fieldName = fieldName.substring(1);
}
Collection<? extends TypeScope> allTypes = ModelUtils.resolveType(model, fieldAccess);
if (allTypes.size() == 1) {
TypeScope type = ModelUtils.getFirst(allTypes);
ElementQuery.Index index = model.getIndexScope().getIndex();
ElementFilter staticFieldsFilter = ElementFilter.allOf(
ElementFilter.forName(NameKind.exact(fieldName)),
ElementFilter.forStaticModifiers(true));
Set<FieldElement> allFields = staticFieldsFilter.filter(index.getAlllFields(type));
if (allFields.isEmpty()) {
assert type != null;
FileObject fileObject = type.getFileObject();
BaseDocument document = fileObject != null ? GsfUtilities.getDocument(fileObject, true) : null;
if (document != null && fileObject.canWrite()) {
if (type instanceof ClassScope || type instanceof TraitScope) {
fix = new IntroduceStaticFieldFix(document, fieldAccess, type);
}
}
}
}
}
}
super.visit(fieldAccess);
}
@Override
public void visit(StaticConstantAccess staticConstantAccess) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineBounds.containsInclusive(staticConstantAccess.getStartOffset())) {
String constName = staticConstantAccess.getConstantName().getName();
String clzName = CodeUtils.extractUnqualifiedClassName(staticConstantAccess);
if (clzName != null && StringUtils.hasText(constName)) {
Collection<? extends TypeScope> allTypes = ModelUtils.resolveType(model, staticConstantAccess);
if (allTypes.size() == 1) {
TypeScope type = ModelUtils.getFirst(allTypes);
// trait can't have constants
if (!(type instanceof TraitScope)) {
ElementQuery.Index index = model.getIndexScope().getIndex();
Set<TypeConstantElement> allConstants = ElementFilter.forName(NameKind.exact(constName)).filter(index.getAllTypeConstants(type));
if (allConstants.isEmpty()) {
assert type != null;
FileObject fileObject = type.getFileObject();
BaseDocument document = fileObject != null ? GsfUtilities.getDocument(fileObject, false) : null;
if (document != null && fileObject.canWrite()) {
fix = new IntroduceClassConstantFix(document, staticConstantAccess, (TypeScope) type);
}
}
}
}
}
}
super.visit(staticConstantAccess);
}
/**
* @return or null
*/
public IntroduceFix getIntroduceFix() {
return fix;
}
private ClassElement getIndexedClass(String name) {
ClassElement retval = null;
ElementQuery.Index index = model.getIndexScope().getIndex();
Collection<ClassElement> classes = Collections.emptyList();
if ("self".equals(name) || "parent".equals(name)) { //NOI18N
ClassDeclaration classDeclaration = null;
for (ASTNode aSTNode : getPath()) {
if (aSTNode instanceof ClassDeclaration) {
classDeclaration = (ClassDeclaration) aSTNode;
break;
}
}
if (classDeclaration != null) {
String clzName = CodeUtils.extractClassName(classDeclaration);
classes = index.getClasses(NameKind.exact(clzName));
}
} else {
classes = index.getClasses(NameKind.exact(name));
}
if (classes.size() == 1) {
retval = classes.iterator().next();
if ("parent".equals(name)) { // NOI18N
QualifiedName superClassQualifiedName = retval.getSuperClassName();
if (superClassQualifiedName != null) {
String superClassName = superClassQualifiedName.getName();
if (superClassName != null) {
classes = index.getClasses(NameKind.exact(superClassName));
retval = (classes.size() == 1) ? classes.iterator().next() : null;
}
}
}
}
return retval;
}
}
private static class IntroduceClassFix extends IntroduceFix {
private final String nsPart;
private final String className;
private final String classNameWithNsPart;
private final FileObject folder;
private final FileObject template;
static IntroduceClassFix getInstance(String className, Model model, ClassInstanceCreation instanceCreation) {
FileObject currentFile = model.getFileScope().getFileObject();
FileObject folder = currentFile == null ? null : currentFile.getParent();
String templatePath = "Templates/Scripting/PHPClass.php"; //NOI18N
FileObject template = FileUtil.getConfigFile(templatePath);
return (template != null && folder != null && folder.canWrite())
? new IntroduceClassFix(className, template, folder, instanceCreation) : null;
}
IntroduceClassFix(String classNameWithNsPart, FileObject template, FileObject folder, ClassInstanceCreation instanceCreation) {
super(null, instanceCreation);
int lastIndexOfNsSeparator = classNameWithNsPart.lastIndexOf(NAMESPACE_SEPARATOR);
this.nsPart = lastIndexOfNsSeparator == -1 ? "" : classNameWithNsPart.substring(0, lastIndexOfNsSeparator);
this.className = classNameWithNsPart.substring(lastIndexOfNsSeparator + 1);
this.classNameWithNsPart = classNameWithNsPart;
this.template = template;
this.folder = folder;
}
@Override
public void implement() throws Exception {
final DataFolder dataFolder = DataFolder.findFolder(folder);
final DataObject configDataObject = DataObject.find(template);
final FileObject[] clsFo = new FileObject[1];
FileUtil.runAtomicAction(new Runnable() {
@Override
public void run() {
try {
Map<String, String> parameters = new HashMap<>();
if (StringUtils.hasText(nsPart)) {
parameters.put(NAMESPACE_PARAMETER_NAME, nsPart); //NOI18N
}
DataObject clsDataObject = configDataObject.createFromTemplate(dataFolder, className, parameters);
clsFo[0] = clsDataObject.getPrimaryFile();
FileObject fo = clsFo[0];
FileLock lock = fo.lock();
try {
fo.rename(lock, fo.getName(), "php"); //NOI18N
} finally {
lock.releaseLock();
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
});
if (clsFo[0] != null) {
UiUtils.open(clsFo[0], 0);
}
}
@Override
@Messages({
"# {0} - Class name",
"# {1} - File name",
"IntroduceHintClassDesc=Create Class \"{0}\" in {1}"
})
public String getDescription() {
String fileName = FileUtil.getFileDisplayName(folder);
int length = fileName.length();
if (length > 30) {
fileName = fileName.substring(length - 30);
final int indexOf = fileName.indexOf("/");
if (indexOf != -1) { //NOI18N
fileName = fileName.substring(indexOf);
}
fileName = String.format("...%s/%s.php", fileName, className); //NOI18N
}
return Bundle.IntroduceHintClassDesc(classNameWithNsPart, fileName);
}
}
private static class IntroduceMethodFix extends IntroduceFix {
private final TypeScope type;
private final MethodDeclarationItem item;
public IntroduceMethodFix(BaseDocument doc, MethodInvocation node, TypeScope type) {
super(doc, node);
this.type = type;
this.item = createMethodDeclarationItem(type, node);
}
@Override
public void implement() throws Exception {
int templateOffset = getOffset();
EditList edits = new EditList(doc);
edits.replace(templateOffset, 0, "\n" + item.getCustomInsertTemplate(), true, 0); //NOI18N
edits.apply();
templateOffset = LineDocumentUtils.getLineEnd(doc, templateOffset + 1);
UiUtils.open(type.getFileObject(), LineDocumentUtils.getLineEnd(doc, templateOffset + 1) - 1);
}
@Override
@Messages({
"# {0} - Method name",
"# {1} - Type kind",
"# {2} - Type name",
"# {3} - File name",
"IntroduceHintMethodDesc=Create Method \"{0}\" in {1} \"{2}\" ({3})"
})
public String getDescription() {
String typeName = type.getName();
FileObject fileObject = type.getFileObject();
String fileName = fileObject == null ? UNKNOWN_FILE_NAME : fileObject.getNameExt();
String typeKindName = getTypeKindName(type);
return Bundle.IntroduceHintMethodDesc(item.getMethod().asString(PrintAs.NameAndParamsDeclaration), typeKindName, typeName, fileName);
}
int getOffset() throws BadLocationException {
return IntroduceSuggestion.getOffset(doc, type, PhpElementKind.METHOD);
}
}
private static class IntroduceStaticMethodFix extends IntroduceFix {
private final TypeScope type;
private final MethodDeclarationItem item;
public IntroduceStaticMethodFix(BaseDocument doc, StaticMethodInvocation node, TypeScope type) {
super(doc, node);
this.type = type;
this.item = createMethodDeclarationItem(type, node);
}
@Override
public void implement() throws Exception {
int templateOffset = getOffset();
EditList edits = new EditList(doc);
edits.replace(templateOffset, 0, "\n" + item.getCustomInsertTemplate(), true, 0); //NOI18N
edits.apply();
templateOffset = LineDocumentUtils.getLineEnd(doc, templateOffset + 1);
UiUtils.open(type.getFileObject(), LineDocumentUtils.getLineEnd(doc, templateOffset + 1) - 1);
}
@Override
@Messages({
"# {0} - Method name",
"# {1} - Type kind",
"# {2} - Type name",
"# {3} - File name",
"IntroduceHintStaticMethodDesc=Create Method \"{0}\" in {1} \"{2}\" ({3})"
})
public String getDescription() {
String typeName = type.getName();
FileObject fileObject = type.getFileObject();
String fileName = fileObject == null ? UNKNOWN_FILE_NAME : fileObject.getNameExt();
String typeKindName = getTypeKindName(type);
return Bundle.IntroduceHintStaticMethodDesc(item.getMethod().asString(PrintAs.NameAndParamsDeclaration), typeKindName, typeName, fileName);
}
int getOffset() throws BadLocationException {
return IntroduceSuggestion.getOffset(doc, type, PhpElementKind.METHOD);
}
}
private static class IntroduceFieldFix extends IntroduceFix {
private final TypeScope type;
private final String templ;
private final VariableBase dispatcher;
private String fieldName;
public IntroduceFieldFix(BaseDocument doc, FieldAccess node, TypeScope type) {
super(doc, node);
this.type = type;
this.dispatcher = node.getDispatcher();
this.templ = createTemplate();
}
@Override
public void implement() throws Exception {
int templateOffset = getOffset();
EditList edits = new EditList(doc);
edits.replace(templateOffset, 0, "\n" + templ, true, 0); //NOI18N
edits.apply();
templateOffset = LineDocumentUtils.getLineEnd(doc, templateOffset + 1) - 2;
UiUtils.open(type.getFileObject(), templateOffset);
}
@Override
@Messages({
"# {0} - Field name",
"# {1} - Type kind",
"# {2} - Type name",
"# {3} - File name",
"IntroduceHintFieldDesc=Create Field \"{0}\" in {1} \"{2}\" ({3})"
})
public String getDescription() {
String typeName = type.getName();
FileObject fileObject = type.getFileObject();
String fileName = fileObject == null ? UNKNOWN_FILE_NAME : fileObject.getNameExt();
String typeKindName = getTypeKindName(type);
return Bundle.IntroduceHintFieldDesc(templ, typeKindName, typeName, fileName);
}
int getOffset() throws BadLocationException {
return IntroduceSuggestion.getOffset(doc, type, PhpElementKind.FIELD);
}
private String createTemplate() {
Variable fieldVar = ((FieldAccess) node).getField();
this.fieldName = CodeUtils.extractVariableName(fieldVar);
if (!fieldVar.isDollared()) {
this.fieldName = "$" + this.fieldName; //NOI18N
}
return String.format("%s %s;", isInternal() ? "private" : "public", fieldName); //NOI18N
}
private boolean isInternal() {
boolean result = false;
if (dispatcher instanceof Variable) {
Variable variable = (Variable) dispatcher;
String dispatcherName = CodeUtils.extractVariableName(variable);
result = "$this".equals(dispatcherName); //NOI18N
}
return result;
}
}
private static class IntroduceStaticFieldFix extends IntroduceFix {
private final TypeScope type;
private final String templ;
private String fieldName;
public IntroduceStaticFieldFix(BaseDocument doc, StaticFieldAccess node, TypeScope type) {
super(doc, node);
this.type = type;
this.templ = createTemplate();
}
@Override
public void implement() throws Exception {
int templateOffset = getOffset();
EditList edits = new EditList(doc);
edits.replace(templateOffset, 0, "\n" + templ, true, 0); //NOI18N
edits.apply();
templateOffset = LineDocumentUtils.getLineEnd(doc, templateOffset + 1) - 2;
UiUtils.open(type.getFileObject(), templateOffset);
}
@Override
@Messages({
"# {0} - Field name",
"# {1} - Type kind",
"# {2} - Type name",
"# {3} - File name",
"IntroduceHintStaticFieldDesc=Create Field \"{0}\" in {1} \"{2}\" ({3})"
})
public String getDescription() {
String typeName = type.getName();
FileObject fileObject = type.getFileObject();
String fileName = fileObject == null ? UNKNOWN_FILE_NAME : fileObject.getNameExt();
String typeKindName = getTypeKindName(type);
return Bundle.IntroduceHintStaticFieldDesc(fieldName, typeKindName, typeName, fileName);
}
int getOffset() throws BadLocationException {
return IntroduceSuggestion.getOffset(doc, type, PhpElementKind.FIELD);
}
private String createTemplate() {
Variable fieldVar = ((StaticFieldAccess) node).getField();
fieldName = CodeUtils.extractVariableName(fieldVar);
if (!fieldVar.isDollared()) {
fieldName = "$" + fieldName; //NOI18N
}
return String.format("static %s = \"\";", fieldName);
}
}
private static class IntroduceClassConstantFix extends IntroduceFix {
private final TypeScope type;
private final String templ;
private final String constantName;
public IntroduceClassConstantFix(BaseDocument doc, StaticConstantAccess node, TypeScope type) {
super(doc, node);
this.type = type;
this.constantName = ((StaticConstantAccess) node).getConstantName().getName();
this.templ = String.format("const %s = \"\";", constantName);
}
@Override
public void implement() throws Exception {
int templateOffset = getOffset();
EditList edits = new EditList(doc);
edits.replace(templateOffset, 0, "\n" + templ, true, 0); //NOI18N
edits.apply();
templateOffset = LineDocumentUtils.getLineEnd(doc, templateOffset + 1) - 2;
UiUtils.open(type.getFileObject(), templateOffset);
}
@Override
@Messages({
"# {0} - Constant name",
"# {1} - Type kind",
"# {2} - Type name",
"# {3} - File name",
"IntroduceHintClassConstDesc=Create Constant \"{0}\" in {1} \"{2}\" ({3})"
})
public String getDescription() {
String typeName = type.getName();
FileObject fileObject = type.getFileObject();
String fileName = fileObject == null ? UNKNOWN_FILE_NAME : fileObject.getNameExt();
String typeKindName = getTypeKindName(type);
return Bundle.IntroduceHintClassConstDesc(constantName, typeKindName, typeName, fileName);
}
int getOffset() throws BadLocationException {
return IntroduceSuggestion.getOffset(doc, type, PhpElementKind.TYPE_CONSTANT);
}
}
abstract static class IntroduceFix implements HintFix {
BaseDocument doc;
ASTNode node;
public IntroduceFix(BaseDocument doc, ASTNode node) {
this.doc = doc;
this.node = node;
}
OffsetRange getOffsetRange() {
return new OffsetRange(node.getStartOffset(), node.getEndOffset());
}
@Override
public boolean isInteractive() {
return false;
}
@Override
public boolean isSafe() {
return true;
}
}
private static String getParameters(final List<Expression> parameters) {
StringBuilder paramNames = new StringBuilder();
for (int i = 0; i < parameters.size(); i++) {
Expression expression = parameters.get(i);
String varName = null;
if (expression instanceof Variable) {
varName = CodeUtils.extractVariableName((Variable) expression);
}
if (varName == null) {
varName = String.format("$param%d", i); //NOI18N
}
if (i > 0) {
paramNames.append(", ");
}
paramNames.append(varName);
}
return paramNames.toString();
}
private static int getOffset(BaseDocument doc, TypeScope typeScope, PhpElementKind kind) throws BadLocationException {
int offset = -1;
Collection<ModelElement> elements = new HashSet<>();
elements.addAll(typeScope.getDeclaredConstants());
switch (kind) {
case METHOD:
if (typeScope instanceof ClassScope) {
ClassScope clz = (ClassScope) typeScope;
elements.addAll(clz.getDeclaredFields());
elements.addAll(clz.getDeclaredMethods());
} else if (typeScope instanceof TraitScope) {
TraitScope trait = (TraitScope) typeScope;
elements.addAll(trait.getDeclaredFields());
elements.addAll(trait.getDeclaredMethods());
}
break;
case FIELD:
if (typeScope instanceof ClassScope) {
ClassScope clz = (ClassScope) typeScope;
elements.addAll(clz.getDeclaredFields());
} else if (typeScope instanceof TraitScope) {
TraitScope trait = (TraitScope) typeScope;
elements.addAll(trait.getDeclaredFields());
}
break;
case TYPE_CONSTANT:
// no-op
break;
default:
assert false;
}
int newOffset;
for (ModelElement elem : elements) {
newOffset = elem.getOffset();
if (elem instanceof MethodScope) {
newOffset = getOffsetAfterBlockCloseCurly(doc, newOffset);
} else {
newOffset = getOffsetAfterNextSemicolon(doc, newOffset);
}
if (newOffset > offset) {
offset = newOffset;
}
}
if (offset == -1) {
if (typeScope.isTraited()) {
// has use trait statements
offset = getOffsetAfterUseTrait(doc, typeScope);
} else {
offset = getOffsetAfterClassOpenCurly(doc, typeScope.getOffset());
}
}
return offset;
}
private static int getOffsetAfterBlockCloseCurly(BaseDocument doc, int offset) throws BadLocationException {
int retval = offset;
doc.readLock();
try {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, retval);
if (ts != null) {
ts.move(retval);
int curlyMatch = 0;
while (ts.moveNext()) {
Token t = ts.token();
if (t.id() == PHPTokenId.PHP_CURLY_OPEN || t.id() == PHPTokenId.PHP_CURLY_CLOSE) {
if (t.id() == PHPTokenId.PHP_CURLY_OPEN) {
curlyMatch++;
} else if (t.id() == PHPTokenId.PHP_CURLY_CLOSE) {
curlyMatch--;
}
if (curlyMatch == 0) {
ts.moveNext();
retval = ts.offset();
break;
}
}
}
}
} finally {
doc.readUnlock();
}
return retval;
}
private static int getOffsetAfterNextSemicolon(BaseDocument doc, int offset) throws BadLocationException {
return getOffsetAfterNextTokenId(doc, offset, PHPTokenId.PHP_SEMICOLON);
}
private static int getOffsetAfterClassOpenCurly(BaseDocument doc, int offset) throws BadLocationException {
return getOffsetAfterNextTokenId(doc, offset, PHPTokenId.PHP_CURLY_OPEN);
}
private static int getOffsetAfterNextTokenId(BaseDocument doc, int offset, PHPTokenId tokenId) throws BadLocationException {
int retval = offset;
doc.readLock();
try {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, retval);
if (ts != null) {
ts.move(retval);
while (ts.moveNext()) {
Token t = ts.token();
if (t.id() == tokenId) {
ts.moveNext();
retval = ts.offset();
break;
}
}
}
} finally {
doc.readUnlock();
}
return retval;
}
/**
* Get an offset after a use trait statement.
* <b>NOTE:</b>This method should be used when a type doesn't have
* constants. If the use trait statement is after fields or methods, the
* offset after the open curry for type is returned.
*
* @param document the document
* @param typeScope the type scope
* @return The offset after the last use trait statement if traits are used,
* otherwise the offset after the open curly for the type
*/
private static int getOffsetAfterUseTrait(BaseDocument document, TypeScope typeScope) {
OffsetRange blockRange = typeScope.getBlockRange();
int offset = blockRange.getEnd() - 1; // before close curly "}"
Collection<ModelElement> elements = new HashSet<>();
elements.addAll(typeScope.getDeclaredMethods());
if (typeScope instanceof ClassScope) {
elements.addAll(((ClassScope) typeScope).getDeclaredFields());
} else if (typeScope instanceof TraitScope) {
elements.addAll(((TraitScope) typeScope).getDeclaredFields());
}
for (ModelElement element : elements) {
int newOffset = element.getOffset();
if (newOffset < offset) {
offset = newOffset;
}
}
document.readLock();
try {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(document, offset);
if (ts != null) {
ts.move(offset);
if (ts.movePrevious()) {
// find the following cases : "use TraitA;", "use TraitA{}" and "TypeName {"
List<PHPTokenId> lookfor = Arrays.asList(
PHPTokenId.PHP_SEMICOLON,
PHPTokenId.PHP_CURLY_CLOSE,
PHPTokenId.PHP_CURLY_OPEN
);
Token<? extends PHPTokenId> previousToken = LexUtilities.findPreviousToken(ts, lookfor);
if (previousToken != null) {
return ts.offset() + previousToken.length();
}
}
}
} finally {
document.readUnlock();
}
return offset;
}
private static PHPCompletionItem.MethodDeclarationItem createMethodDeclarationItem(final TypeScope typeScope, final MethodInvocation node) {
final String methodName = CodeUtils.extractFunctionName(node.getMethod());
final MethodElement method = MethodElementImpl.createMagicMethod(typeScope,
methodName, 0, getParameters(node.getMethod().getParameters()));
return typeScope.isInterface()
? PHPCompletionItem.MethodDeclarationItem.forIntroduceInterfaceHint(method, null)
: PHPCompletionItem.MethodDeclarationItem.forIntroduceHint(method, null);
}
private static PHPCompletionItem.MethodDeclarationItem createMethodDeclarationItem(final TypeScope typeScope, final StaticMethodInvocation node) {
final String methodName = CodeUtils.extractFunctionName(node.getMethod());
final MethodElement method = MethodElementImpl.createMagicMethod(typeScope, methodName,
Modifier.STATIC, getParameters(node.getMethod().getParameters()));
return PHPCompletionItem.MethodDeclarationItem.forIntroduceHint(method, null);
}
@Messages({
"IntroduceHintClassName=Class",
"IntroduceHintInterfaceName=Interface",
"IntroduceHintTraitName=Trait"
})
private static String getTypeKindName(TypeScope typeScope) {
if (typeScope instanceof ClassScope) {
return Bundle.IntroduceHintClassName();
} else if (typeScope instanceof InterfaceScope) {
return Bundle.IntroduceHintInterfaceName();
} else if (typeScope instanceof TraitScope) {
return Bundle.IntroduceHintTraitName();
}
assert false;
return "?"; // NOI18N
}
}