/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.engine.internal.hint;
import com.google.dart.engine.ast.AstNode;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.ast.ImportDirective;
import com.google.dart.engine.ast.StringLiteral;
import com.google.dart.engine.ast.visitor.RecursiveAstVisitor;
import com.google.dart.engine.context.AnalysisContext;
import com.google.dart.engine.element.CompilationUnitElement;
import com.google.dart.engine.error.PubSuggestionCode;
import com.google.dart.engine.internal.error.ErrorReporter;
import com.google.dart.engine.source.FileUriResolver;
import com.google.dart.engine.source.PackageUriResolver;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.utilities.general.StringUtilities;
import java.io.File;
/**
* Instances of the class {@code PubVerifier} traverse an AST structure looking for deviations from
* pub best practices.
*/
public class PubVerifier extends RecursiveAstVisitor<Void> {
private static final String PUBSPEC_YAML = "pubspec.yaml";
/**
* The analysis context containing the sources to be analyzed
*/
private final AnalysisContext context;
/**
* The error reporter by which errors will be reported.
*/
private final ErrorReporter errorReporter;
public PubVerifier(AnalysisContext context, ErrorReporter errorReporter) {
this.context = context;
this.errorReporter = errorReporter;
}
@Override
public Void visitImportDirective(ImportDirective directive) {
// Don't bother showing a suggestion if there is a more important issue to be solved.
StringLiteral uriLiteral = directive.getUri();
if (uriLiteral == null) {
return null;
}
String uriContent = uriLiteral.getStringValue();
if (uriContent == null) {
return null;
}
uriContent = uriContent.trim();
// Analyze the URI
int index = uriContent.indexOf(':');
String scheme;
String path;
if (index > -1) {
scheme = uriContent.substring(0, index);
path = uriContent.substring(index + 1);
} else {
scheme = FileUriResolver.FILE_SCHEME;
path = uriContent;
}
if (scheme.equals(FileUriResolver.FILE_SCHEME)) {
if (!checkForFileImportOutsideLibReferencesFileInside(uriLiteral, path)) {
checkForFileImportInsideLibReferencesFileOutside(uriLiteral, path);
}
} else if (scheme.equals(PackageUriResolver.PACKAGE_SCHEME)) {
checkForPackageImportContainsDotDot(uriLiteral, path);
}
return null;
}
/**
* This verifies that the passed file import directive is not contained in a source inside a
* package "lib" directory hierarchy referencing a source outside that package "lib" directory
* hierarchy.
*
* @param uriLiteral the import URL (not {@code null})
* @param path the file path being verified (not {@code null})
* @return {@code true} if and only if an error code is generated on the passed node
* @see PubSuggestionCode.FILE_IMPORT_INSIDE_LIB_REFERENCES_FILE_OUTSIDE
*/
private boolean checkForFileImportInsideLibReferencesFileOutside(StringLiteral uriLiteral,
String path) {
Source source = getSource(uriLiteral);
String fullName = getSourceFullName(source);
if (fullName != null) {
int pathIndex = 0;
int fullNameIndex = fullName.length();
while (pathIndex < path.length()
&& StringUtilities.startsWith3(path, pathIndex, '.', '.', '/')) {
fullNameIndex = fullName.lastIndexOf('/', fullNameIndex);
if (fullNameIndex < 4) {
return false;
}
// Check for "/lib" at a specified place in the fullName
if (StringUtilities.startsWith4(fullName, fullNameIndex - 4, '/', 'l', 'i', 'b')) {
String relativePubspecPath = path.substring(0, pathIndex + 3).concat(PUBSPEC_YAML);
Source pubspecSource = context.getSourceFactory().resolveUri(source, relativePubspecPath);
if (context.exists(pubspecSource)) {
// Files inside the lib directory hierarchy should not reference files outside
errorReporter.reportErrorForNode(
PubSuggestionCode.FILE_IMPORT_INSIDE_LIB_REFERENCES_FILE_OUTSIDE,
uriLiteral);
}
return true;
}
pathIndex += 3;
}
}
return false;
}
/**
* This verifies that the passed file import directive is not contained in a source outside a
* package "lib" directory hierarchy referencing a source inside that package "lib" directory
* hierarchy.
*
* @param uriLiteral the import URL (not {@code null})
* @param path the file path being verified (not {@code null})
* @return {@code true} if and only if an error code is generated on the passed node
* @see PubSuggestionCode.FILE_IMPORT_OUTSIDE_LIB_REFERENCES_FILE_INSIDE
*/
private boolean checkForFileImportOutsideLibReferencesFileInside(StringLiteral uriLiteral,
String path) {
if (StringUtilities.startsWith4(path, 0, 'l', 'i', 'b', '/')) {
if (checkForFileImportOutsideLibReferencesFileInsideAtIndex(uriLiteral, path, 0)) {
return true;
}
}
int pathIndex = StringUtilities.indexOf5(path, 0, '/', 'l', 'i', 'b', '/');
while (pathIndex != -1) {
if (checkForFileImportOutsideLibReferencesFileInsideAtIndex(uriLiteral, path, pathIndex + 1)) {
return true;
}
pathIndex = StringUtilities.indexOf5(path, pathIndex + 4, '/', 'l', 'i', 'b', '/');
}
return false;
}
private boolean checkForFileImportOutsideLibReferencesFileInsideAtIndex(StringLiteral uriLiteral,
String path, int pathIndex) {
Source source = getSource(uriLiteral);
String relativePubspecPath = path.substring(0, pathIndex).concat(PUBSPEC_YAML);
Source pubspecSource = context.getSourceFactory().resolveUri(source, relativePubspecPath);
if (!context.exists(pubspecSource)) {
return false;
}
String fullName = getSourceFullName(source);
if (fullName != null) {
if (StringUtilities.indexOf5(fullName, 0, '/', 'l', 'i', 'b', '/') < 0) {
// Files outside the lib directory hierarchy should not reference files inside
// ... use package: url instead
errorReporter.reportErrorForNode(
PubSuggestionCode.FILE_IMPORT_OUTSIDE_LIB_REFERENCES_FILE_INSIDE,
uriLiteral);
return true;
}
}
return false;
}
/**
* This verifies that the passed package import directive does not contain ".."
*
* @param uriLiteral the import URL (not {@code null})
* @param path the path to be validated (not {@code null})
* @return {@code true} if and only if an error code is generated on the passed node
* @see PubSuggestionCode.PACKAGE_IMPORT_CONTAINS_DOT_DOT
*/
private boolean checkForPackageImportContainsDotDot(StringLiteral uriLiteral, String path) {
if (StringUtilities.startsWith3(path, 0, '.', '.', '/')
|| StringUtilities.indexOf4(path, 0, '/', '.', '.', '/') >= 0) {
// Package import should not to contain ".."
errorReporter.reportErrorForNode(PubSuggestionCode.PACKAGE_IMPORT_CONTAINS_DOT_DOT, uriLiteral);
return true;
}
return false;
}
/**
* Answer the source associated with the compilation unit containing the given AST node.
*
* @param node the node (not {@code null})
* @return the source or {@code null} if it could not be determined
*/
private Source getSource(AstNode node) {
Source source = null;
CompilationUnit unit = node.getAncestor(CompilationUnit.class);
if (unit != null) {
CompilationUnitElement element = unit.getElement();
if (element != null) {
source = element.getSource();
}
}
return source;
}
/**
* Answer the full name of the given source. The returned value will have all
* {@link File#separatorChar} replace by '/'.
*
* @param source the source
* @return the full name or {@code null} if it could not be determined
*/
private String getSourceFullName(Source source) {
if (source != null) {
String fullName = source.getFullName();
if (fullName != null) {
return fullName.replace(File.separatorChar, '/');
}
}
return null;
}
}