/******************************************************************************* * Copyright (c) 2017 Alex Xu and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alex Xu - initial API and implementation *******************************************************************************/ package org.eclipse.php.internal.core.compiler.ast.visitor; import java.text.MessageFormat; import java.util.*; import java.util.regex.Matcher; import org.eclipse.dltk.ast.ASTNode; import org.eclipse.dltk.ast.declarations.ModuleDeclaration; import org.eclipse.dltk.ast.declarations.TypeDeclaration; import org.eclipse.dltk.ast.references.TypeReference; import org.eclipse.dltk.ast.statements.Statement; import org.eclipse.dltk.compiler.problem.*; import org.eclipse.dltk.core.*; import org.eclipse.dltk.core.builder.IBuildContext; import org.eclipse.dltk.core.builder.ISourceLineTracker; import org.eclipse.php.core.PHPVersion; import org.eclipse.php.core.compiler.PHPFlags; import org.eclipse.php.core.compiler.ast.nodes.*; import org.eclipse.php.core.compiler.ast.visitor.PHPASTVisitor; import org.eclipse.php.core.project.ProjectOptions; import org.eclipse.php.internal.core.PHPCorePlugin; import org.eclipse.php.internal.core.codeassist.PHPSelectionEngine; import org.eclipse.php.internal.core.compiler.ast.parser.Messages; import org.eclipse.php.internal.core.compiler.ast.parser.PHPProblemIdentifier; import org.eclipse.php.internal.core.typeinference.PHPModelUtils; import org.eclipse.php.internal.core.typeinference.PHPSimpleTypes; import org.eclipse.php.internal.core.typeinference.evaluators.PHPEvaluationUtils; import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities; public class ValidatorVisitor extends PHPASTVisitor { private static final String PAAMAYIM_NEKUDOTAIM = "::"; //$NON-NLS-1$ private static final List<String> TYPE_SKIP = new ArrayList<String>(); private static final List<String> PHPDOC_TYPE_SKIP = new ArrayList<String>(); static { TYPE_SKIP.add("parent"); //$NON-NLS-1$ TYPE_SKIP.add("self"); //$NON-NLS-1$ TYPE_SKIP.add("static"); //$NON-NLS-1$ TYPE_SKIP.add("null"); //$NON-NLS-1$ PHPDOC_TYPE_SKIP.add("true"); //$NON-NLS-1$ PHPDOC_TYPE_SKIP.add("false"); //$NON-NLS-1$ } private Map<String, UsePartInfo> usePartInfo = new LinkedHashMap<String, UsePartInfo>(); private Map<String, Boolean> elementExists = new HashMap<String, Boolean>(); private NamespaceDeclaration currentNamespace; private Set<String> typeDeclared = new HashSet<>(); private boolean hasNamespace; private ISourceModule sourceModule; private PHPVersion version; private IBuildContext context; @SuppressWarnings("null") public ValidatorVisitor(IBuildContext context) { this.context = context; this.sourceModule = context.getSourceModule(); this.version = ProjectOptions.getPHPVersion(sourceModule); } @Override public boolean endvisit(ModuleDeclaration s) throws Exception { if (!hasNamespace) { checkUnusedImport(); } return super.endvisit(s); } @Override public boolean visit(NamespaceDeclaration s) throws Exception { hasNamespace = true; currentNamespace = s; return true; } @Override public boolean endvisit(NamespaceDeclaration s) throws Exception { checkUnusedImport(); return super.endvisit(s); } @Override public boolean visit(PHPMethodDeclaration s) throws Exception { if (s.getPHPDoc() != null) s.getPHPDoc().traverse(this); return visitGeneral(s); } @Override public boolean visit(PHPFieldDeclaration s) throws Exception { if (s.getPHPDoc() != null) { s.getPHPDoc().traverse(this); } return super.visit(s); } @Override public boolean visit(PHPCallExpression node) throws Exception { if (node.getReceiver() != null) { node.getReceiver().traverse(this); } if (node.getArgs() != null) { node.getArgs().traverse(this); } return false; } @Override public boolean visit(FullyQualifiedReference node) throws Exception { return visit((TypeReference) node); } @Override public boolean visit(TypeReference node) throws Exception { return visit(node, ProblemSeverities.Error, false); } private boolean visit(TypeReference node, ProblemSeverity severity, boolean isInDoc) throws Exception { boolean skip = false; if (isInDoc) { skip = PHPSimpleTypes.isSimpleType(node.getName()); } else { skip = PHPSimpleTypes.isHintable(node.getName(), version); } if (skip || TYPE_SKIP.contains(node.getName().toLowerCase())) { return true; } TypeReferenceInfo tri = new TypeReferenceInfo(node, false); String nodeName = tri.getTypeName(); String key = null; if (tri.isGlobal()) { key = nodeName; } else { key = getFirstSegmentOfTypeName(nodeName); } UsePartInfo info = usePartInfo.get(key.toLowerCase()); if (info != null) { info.refer(); } boolean isFound = findElement(tri); if (!isFound) { reportProblem(node, Messages.UndefinedType, PHPProblemIdentifier.UndefinedType, node.getName(), severity); } return false; } @Override public boolean visit(AnonymousClassDeclaration s) throws Exception { int end = s.start(); int start = end - 1; while (sourceModule.getSource().charAt(start) != ' ') { start--; } checkUnimplementedMethods(s, start + 1, end); IModelElement element = sourceModule.getElementAt(s.start()); if (s.getSuperClass() != null && element != null) checkSuperclass(s.getSuperClass(), false, element.getElementName()); Collection<TypeReference> interfaces = s.getInterfaceList(); if (interfaces != null && element != null) { for (TypeReference itf : interfaces) checkSuperclass(itf, true, element.getElementName()); } return super.visit(s); } @Override public boolean visit(ClassDeclaration s) throws Exception { checkUnimplementedMethods(s, s.getRef()); if (s.getSuperClass() != null) checkSuperclass(s.getSuperClass(), false, s.getName()); Collection<TypeReference> interfaces = s.getInterfaceList(); if (interfaces != null && interfaces.size() > 0) { for (TypeReference itf : interfaces) checkSuperclass(itf, true, s.getName()); } return true; } @Override public boolean visit(ClassInstanceCreation s) throws Exception { if (s.getClassName() instanceof TypeReference) { TypeReferenceInfo tri = new TypeReferenceInfo((TypeReference) s.getClassName(), false); IType[] types = PHPModelUtils.getTypes(tri.getFullyQualifiedName(), sourceModule, s.getClassName().sourceStart(), null); for (IType type : types) { if (PHPFlags.isInterface(type.getFlags()) || PHPFlags.isAbstract(type.getFlags())) { reportProblem(s.getClassName(), Messages.CannotInstantiateType, PHPProblemIdentifier.CannotInstantiateType, type.getElementName(), ProblemSeverities.Error); break; } } } return visitGeneral(s); } @Override public boolean visit(InterfaceDeclaration s) throws Exception { if (s.getSuperClasses() == null) return true; for (ASTNode node : s.getSuperClasses().getChilds()) { checkSuperclass((TypeReference) node, true, s.getName()); } return true; } @Override public boolean visit(TypeDeclaration s) throws Exception { if (!(s instanceof NamespaceDeclaration)) { checkDuplicateTypeDeclaration(s); } return super.visit(s); } public boolean visit(UsePart part) throws Exception { UsePartInfo info = new UsePartInfo(part); TypeReferenceInfo tri = info.getTypeReferenceInfo(); String name = tri.getTypeName(); String currentNamespaceName; if (currentNamespace == null) { currentNamespaceName = ""; //$NON-NLS-1$ } else { currentNamespaceName = currentNamespace.getName(); } String lcName = info.getRealName().toLowerCase(); if (!findElement(tri)) { info.isProblemReported = true; reportProblem(tri.getTypeReference(), Messages.ImportNotFound, PHPProblemIdentifier.ImportNotFound, name, ProblemSeverities.Error); } else if (usePartInfo.get(lcName) != null) { info.isProblemReported = true; reportProblem(tri.getTypeReference(), Messages.DuplicateImport, PHPProblemIdentifier.DuplicateImport, new String[] { name, info.getRealName() }, ProblemSeverities.Error); } else if (!info.isAlias && info.getNamespaceName().equals(currentNamespaceName)) { info.isProblemReported = true; reportProblem(tri.getTypeReference(), Messages.UnnecessaryImport, PHPProblemIdentifier.UnnecessaryImport, new String[] { name }, ProblemSeverities.Warning); } usePartInfo.put(lcName, info); return false; } /** * Generic checks to only visit PHPDoc type references whose names are valid * php identifier names. See also * {@link PHPSelectionEngine#lookForMatchingElements()} for more complete * and precise PHPDoc type references handling. */ @SuppressWarnings("null") private void visitPHPDocType(TypeReference typeReference, ProblemSeverity severity) throws Exception { if (PHPDOC_TYPE_SKIP.contains(typeReference.getName().toLowerCase())) { return; } String name = typeReference.getName(); assert name.length() > 0; int idx = name.indexOf(PAAMAYIM_NEKUDOTAIM); if (idx == -1) { // look if complete name is a valid identifier name if (PHPTextSequenceUtilities.readIdentifierStartIndex(version, name, name.length(), false) == 0) { visit(typeReference, severity, true); } } else if (idx > 0) { // look if name part before "::" is a valid (and non-empty) // identifier name if (PHPTextSequenceUtilities.readIdentifierStartIndex(version, name, idx, false) == 0) { visit(new TypeReference(typeReference.start(), typeReference.start() + idx, name.substring(0, idx)), severity, true); } } } @Override public boolean visit(PHPDocTag phpDocTag) throws Exception { for (TypeReference fullTypeReference : phpDocTag.getTypeReferences()) { String fullTypeName = fullTypeReference.getName(); // look for all type names inside fullTypeName by removing all type // delimiters, regardless if fullTypeName's content respects PHPDoc // standards or not Matcher matcher = PHPEvaluationUtils.TYPE_DELIMS_PATTERN.matcher(fullTypeName); int start = 0; while (matcher.find()) { if (matcher.start() != start) { String typeName = fullTypeName.substring(start, matcher.start()); TypeReference typeReference = new TypeReference(fullTypeReference.start() + start, fullTypeReference.start() + start + typeName.length(), typeName); visitPHPDocType(typeReference, ProblemSeverities.Warning); } start = matcher.end(); } if (start == 0) { visitPHPDocType(fullTypeReference, ProblemSeverities.Warning); } else if (start != fullTypeName.length()) { String typeName = fullTypeName.substring(start); TypeReference typeReference = new TypeReference(fullTypeReference.start() + start, fullTypeReference.start() + start + typeName.length(), typeName); visitPHPDocType(typeReference, ProblemSeverities.Warning); } } return false; } private void checkDuplicateTypeDeclaration(TypeDeclaration node) { String name = node.getName(); String currentNamespaceName; if (currentNamespace == null) { currentNamespaceName = ""; //$NON-NLS-1$ } else { currentNamespaceName = currentNamespace.getName(); } boolean isDuplicateWithUse = false; UsePartInfo info = usePartInfo.get(name.toLowerCase()); if (info != null) { String fullyQualifiedName = PHPModelUtils.concatFullyQualifiedNames(NamespaceReference.NAMESPACE_DELIMITER, currentNamespaceName, name); if (!info.getFullyQualifiedName().equals(fullyQualifiedName)) { isDuplicateWithUse = true; } } if (isDuplicateWithUse || !typeDeclared.add(name.toLowerCase())) { reportProblem(node.getRef(), Messages.DuplicateDeclaration, PHPProblemIdentifier.DuplicateDeclaration, name, ProblemSeverities.Error); } } private void checkUnimplementedMethods(Statement statement, ASTNode classNode) throws ModelException { checkUnimplementedMethods(statement, classNode.sourceStart(), classNode.sourceEnd()); } private void checkUnimplementedMethods(Statement statement, int nodeStart, int nodeEnd) throws ModelException { IModelElement element = sourceModule.getElementAt(statement.start()); if (!(element instanceof IType)) return; IType type = (IType) element; if (type.getSuperClasses().length > 0 && !PHPFlags.isAbstract(type.getFlags())) { IMethod[] methods = PHPModelUtils.getUnimplementedMethods(type, null); for (IMethod method : methods) { if (method.getParent().getElementName().equals(type.getElementName())) { continue; } StringBuilder methodName = new StringBuilder(method.getParent().getElementName()).append("::"); //$NON-NLS-1$ PHPModelUtils.getMethodLabel(method, methodName); String message = Messages.getString("AbstractMethodMustBeImplemented", new String[] { type.getElementName(), methodName.toString() }); reportProblem(nodeStart, nodeEnd, message, PHPProblemIdentifier.AbstractMethodMustBeImplemented, ProblemSeverities.Error); } } } private void checkSuperclass(TypeReference superClass, boolean isInterface, String className) throws ModelException { if (superClass != null) { TypeReferenceInfo refInfo = new TypeReferenceInfo(superClass, false); IType[] types = PHPModelUtils.getTypes(refInfo.getFullyQualifiedName(), sourceModule, superClass.sourceStart(), null); for (IType type : types) { if (!isInterface) { if (PHPFlags.isInterface(type.getFlags())) { reportProblem(superClass, Messages.SuperclassMustBeAClass, PHPProblemIdentifier.SuperclassMustBeAClass, new String[] { superClass.getName(), className }, ProblemSeverities.Error); } if (PHPFlags.isFinal(type.getFlags())) { reportProblem(superClass, Messages.ClassExtendFinalClass, PHPProblemIdentifier.ClassExtendFinalClass, new String[] { className, type.getElementName() }, ProblemSeverities.Error); } } else { if (!PHPFlags.isInterface(type.getFlags())) { reportProblem(superClass, Messages.SuperInterfaceMustBeAnInterface, PHPProblemIdentifier.SuperInterfaceMustBeAnInterface, new String[] { superClass.getName(), className }, ProblemSeverities.Error); } } } } } private void checkUnusedImport() { Collection<UsePartInfo> useInfos = usePartInfo.values(); for (UsePartInfo useInfo : useInfos) { if (!useInfo.isProblemReported && useInfo.getRefCount() == 0) { FullyQualifiedReference m = useInfo.getUsePart().getNamespace(); String name = m.getFullyQualifiedName(); reportProblem(m, Messages.UnusedImport, PHPProblemIdentifier.UnusedImport, name, ProblemSeverities.Warning); } } usePartInfo.clear(); elementExists.clear(); typeDeclared.clear(); } private boolean findElement(TypeReferenceInfo info) { String name = info.getFullyQualifiedName(); if (elementExists.containsKey(name)) { return elementExists.get(name); } boolean isFound = false; try { TypeReference type = info.getTypeReference(); IModelElement[] types = PHPModelUtils.getTypes(name, sourceModule, type.start(), null); if (types.length == 0) { types = PHPModelUtils.getTraits(name, sourceModule, type.start(), null, null); } if (types.length == 0 && info.isUseStatement()) { types = sourceModule.codeSelect(type.start(), type.end() - type.start()); } if (types.length > 0) { isFound = true; } } catch (ModelException e) { PHPCorePlugin.log(e); } elementExists.put(name, isFound); return isFound; } private void reportProblem(ASTNode s, String message, IProblemIdentifier id, String[] stringArguments, ProblemSeverity severity) { message = MessageFormat.format(message, (Object[]) stringArguments); reportProblem(s, message, id, severity); } private void reportProblem(ASTNode s, String message, IProblemIdentifier id, ProblemSeverity severity) { int start = 0, end = 0; if (s != null) { start = s.sourceStart(); end = s.sourceEnd(); } else { start = end = context.getLineTracker().getLineOffset(1); } reportProblem(start, end, message, id, severity); } private void reportProblem(ASTNode s, String message, IProblemIdentifier id, String stringArguments, ProblemSeverity severity) { reportProblem(s, message, id, new String[] { stringArguments }, severity); } private void reportProblem(int start, int end, String message, IProblemIdentifier id, ProblemSeverity severity) { ISourceLineTracker tracker = context.getLineTracker(); int line = tracker.getLineNumberOfOffset(start); IProblem problem = new DefaultProblem(context.getFile().getName(), message, id, null, severity, start, end, line, 0); context.getProblemReporter().reportProblem(problem); } private String getFirstSegmentOfTypeName(String typeName) { if (typeName != null) { String[] segments = typeName.split("\\\\"); //$NON-NLS-1$ for (String segment : segments) { if (segment.trim().length() > 0) { return segment; } } } return ""; //$NON-NLS-1$ } private class UsePartInfo { private UsePart usePart; private String realName; private int refCount; private String fullyQualifiedName; private TypeReferenceInfo tri; private boolean isAlias = false; private boolean isProblemReported = false; public UsePartInfo(UsePart usePart) { this.usePart = usePart; tri = new TypeReferenceInfo(usePart.getNamespace(), true); if (usePart.getAlias() != null) { realName = usePart.getAlias().getName(); isAlias = true; } else { realName = usePart.getNamespace().getName(); } if (tri.getNamespaceName() != null) { fullyQualifiedName = tri.getNamespaceName(); } fullyQualifiedName = PHPModelUtils.concatFullyQualifiedNames(fullyQualifiedName, usePart.getNamespace().getName()); if (fullyQualifiedName.length() > 0 && fullyQualifiedName.charAt(0) != NamespaceReference.NAMESPACE_SEPARATOR) { fullyQualifiedName = NamespaceReference.NAMESPACE_SEPARATOR + fullyQualifiedName; } } public UsePart getUsePart() { return usePart; } public int getRefCount() { return refCount; } public void refer() { refCount++; } public String getRealName() { return realName; } public String getFullyQualifiedName() { return fullyQualifiedName; } public String getNamespaceName() { return tri.getNamespaceName(); } public TypeReferenceInfo getTypeReferenceInfo() { return tri; } @Override public String toString() { String str = "use " + fullyQualifiedName; //$NON-NLS-1$ if (isAlias) { str += " as " + realName; //$NON-NLS-1$ } return str; } } private class TypeReferenceInfo { private TypeReference typeReference; private boolean isGlobal = false; private boolean hasNamespace = false; private String namespaceName = ""; //$NON-NLS-1$ private String typeName; private String fullyQualifiedName; private boolean isUseStatement; public TypeReferenceInfo(TypeReference typeReference, boolean isUseStatement) { this.typeReference = typeReference; this.isUseStatement = isUseStatement; FullyQualifiedReference fullTypeReference = null; if (typeReference instanceof FullyQualifiedReference) { fullTypeReference = (FullyQualifiedReference) typeReference; if (fullTypeReference.getNamespace() != null) { hasNamespace = true; namespaceName = fullTypeReference.getNamespace().getName(); // for use statement, no need to lookup the use statement // to compute namespace name if (!isUseStatement) { UsePartInfo info = usePartInfo.get(namespaceName.toLowerCase()); if (info != null) { namespaceName = info.getFullyQualifiedName(); } } } } if (fullTypeReference != null && hasNamespace) { isGlobal = fullTypeReference.getNamespace().isGlobal(); typeName = fullTypeReference.getFullyQualifiedName(); } else { typeName = typeReference.getName(); } if (fullTypeReference != null && isGlobal) { fullyQualifiedName = fullTypeReference.getFullyQualifiedName(); } else if (hasNamespace) { fullyQualifiedName = PHPModelUtils.concatFullyQualifiedNames(namespaceName, typeReference.getName()); } else { fullyQualifiedName = typeName; } if (!isUseStatement && !fullyQualifiedName.startsWith(NamespaceReference.NAMESPACE_DELIMITER)) { String key = getFirstSegmentOfTypeName(fullyQualifiedName).toLowerCase(); if (usePartInfo.containsKey(key)) { fullyQualifiedName = usePartInfo.get(key).getFullyQualifiedName(); } else if (currentNamespace != null) { fullyQualifiedName = PHPModelUtils.concatFullyQualifiedNames(currentNamespace.getName(), fullyQualifiedName); } } if (fullyQualifiedName.length() > 0 && fullyQualifiedName.charAt(0) != NamespaceReference.NAMESPACE_SEPARATOR) { fullyQualifiedName = NamespaceReference.NAMESPACE_SEPARATOR + fullyQualifiedName; } } public boolean isGlobal() { return isGlobal; } public String getTypeName() { return typeName; } public String getFullyQualifiedName() { return fullyQualifiedName; } public String getNamespaceName() { return namespaceName; } public TypeReference getTypeReference() { return typeReference; } public boolean isUseStatement() { return isUseStatement; } } }