/*
* Copyright 2009-2017 the original author or authors.
*
* Licensed 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.codehaus.jdt.groovy.model;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.eclipse.GroovyLogManager;
import org.codehaus.groovy.eclipse.TraceCategory;
import org.codehaus.jdt.groovy.integration.internal.MultiplexingSourceElementRequestorParser;
import org.codehaus.jdt.groovy.internal.compiler.ast.GroovyCompilationUnitDeclaration;
import org.codehaus.jdt.groovy.model.ModuleNodeMapper.ModuleNodeInfo;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.PerformanceStats;
import org.eclipse.jdt.core.CompletionRequestor;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModelStatusConstants;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.util.CompilerUtils;
import org.eclipse.jdt.groovy.core.util.JavaConstants;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
import org.eclipse.jdt.internal.compiler.SourceElementParser;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.core.ASTHolderCUInfo;
import org.eclipse.jdt.internal.core.CompilationUnit;
import org.eclipse.jdt.internal.core.CompilationUnitElementInfo;
import org.eclipse.jdt.internal.core.CompilationUnitProblemFinder;
import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaModelManager.PerWorkingCopyInfo;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.OpenableElementInfo;
import org.eclipse.jdt.internal.core.PackageFragment;
import org.eclipse.jdt.internal.core.ReconcileWorkingCopyOperation;
import org.eclipse.jdt.internal.core.util.Util;
/**
* @author Andrew Eisenberg
* @created Jun 2, 2009
*/
public class GroovyCompilationUnit extends CompilationUnit {
private class GroovyErrorHandlingPolicy implements IErrorHandlingPolicy {
final boolean stopOnFirst;
public GroovyErrorHandlingPolicy(boolean stopOnFirst) {
this.stopOnFirst = stopOnFirst;
}
public boolean proceedOnErrors() {
return !stopOnFirst;
}
public boolean stopOnFirstError() {
return stopOnFirst;
}
public boolean ignoreAllErrors() {
// TODO is this the right decision here? New method with java8 support
return false;
}
}
public GroovyCompilationUnit(PackageFragment parent, String name, WorkingCopyOwner owner) {
super(parent, name, owner);
}
/**
* Returns the module node for this GroovyCompilationUnit creates one if one doesn't exist.
*
* This is potentially a long running operation. This method ensures that this CompilationUnit is a working copy and that it is
* consistent (if not a reconcile operation is performed).
*/
public ModuleNode getModuleNode() {
ModuleNodeInfo moduleInfo = getModuleInfo(true);
return moduleInfo != null ? moduleInfo.module : null;
}
/**
* Gets the module info for this compilation unit
*
* @param force if true, then a module info is created even if not a working copy. This occurs by temporarily turning the
* compilation unit into a working copy and then discarding it.
* @return the {@link ModuleNodeInfo} for this compilation unit. Will be null if force is set to false and this unit is not a
* working copy. Also will be null if a problem occurs
*/
public ModuleNodeInfo getModuleInfo(boolean force) {
try {
if (!isConsistent()) {
makeConsistent(null);
}
boolean becameWorkingCopy = false;
ModuleNodeMapper.getInstance().lock();
// discard the working copy after finishing
// if there was no working copy to begin with
try {
becameWorkingCopy = (force && !isWorkingCopy());
if (becameWorkingCopy) {
becomeWorkingCopy(null);
}
PerWorkingCopyInfo info = getPerWorkingCopyInfo();
if (info != null) {
return ModuleNodeMapper.getInstance().get(info);
}
} finally {
try {
if (becameWorkingCopy) {
discardWorkingCopy();
}
} finally {
ModuleNodeMapper.getInstance().unlock();
}
}
} catch (JavaModelException e) {
Util.log(e, "Exception thrown when trying to get Groovy module node for " + this.getElementName());
}
// return null if not found. Means that there was a problem with build structure
return null;
}
/**
* Gets the module node for this compilation unit. Bypasses the cached module node and creates a new one, which is then placed
* in the cache
*/
public ModuleNodeInfo getNewModuleInfo() {
try {
openWhenClosed(createElementInfo(), false/* or should it be true... ? */, new NullProgressMonitor());
} catch (JavaModelException e) {
Util.log(e, "Exception thrown when trying to get Groovy module node for " + this.getElementName());
}
return getModuleInfo(true);
}
@Override
public void discardWorkingCopy() throws JavaModelException {
// GRECLIPSE-804 must synchronize
ModuleNodeMapper.getInstance().lock();
try {
PerWorkingCopyInfo info = getPerWorkingCopyInfo();
if (workingCopyInfoWillBeDiscarded(info)) {
ModuleNodeMapper.getInstance().remove(info);
}
super.discardWorkingCopy();
} finally {
ModuleNodeMapper.getInstance().unlock();
}
}
/**
* working copy info is about to be discared if useCount <= 1
*/
private boolean workingCopyInfoWillBeDiscarded(PerWorkingCopyInfo info) {
return info != null && ((Integer) ReflectionUtils.getPrivateField(PerWorkingCopyInfo.class, "useCount", info)).intValue() <= 1;
}
/**
* Tracks how deep we are in recursive calls to {@link #buildStructure}.
*/
private static final ThreadLocalAtomicInteger depth = new ThreadLocalAtomicInteger();
private static class ThreadLocalAtomicInteger extends ThreadLocal<AtomicInteger> {
@Override
protected AtomicInteger initialValue() {
return new AtomicInteger();
}
int intValue() {
return get().get();
}
void increment() {
get().incrementAndGet();
}
void decrement() {
get().decrementAndGet();
}
}
@Override
protected boolean buildStructure(OpenableElementInfo info, IProgressMonitor pm, Map newElements, IResource underlyingResource)
throws JavaModelException {
depth.increment();
try {
if (GroovyLogManager.manager.hasLoggers()) {
GroovyLogManager.manager.log(TraceCategory.COMPILER, "Build Structure starting for " + name);
GroovyLogManager.manager.logStart("Build structure: " + name + " : " + Thread.currentThread().getName());
}
// ensure buffer is opened
IBuffer buffer = getBufferManager().getBuffer(this);
if (buffer == null) {
openBuffer(pm, info); // open buffer independently from the info, since we are building the info
}
// generate structure and compute syntax problems if needed
GroovyCompilationUnitStructureRequestor requestor = new GroovyCompilationUnitStructureRequestor(this, (CompilationUnitElementInfo) info, newElements);
JavaModelManager.PerWorkingCopyInfo perWorkingCopyInfo = getPerWorkingCopyInfo();
JavaProject project = (JavaProject) getJavaProject();
// determine what kind of buildStructure we are doing
boolean createAST;
int reconcileFlags;
boolean resolveBindings;
HashMap<String, CategorizedProblem[]> problems;
if (info instanceof ASTHolderCUInfo) {
ASTHolderCUInfo astHolder = (ASTHolderCUInfo) info;
createAST = ((Integer) ReflectionUtils.getPrivateField(ASTHolderCUInfo.class, "astLevel", astHolder)) != NO_AST;
resolveBindings = (Boolean) ReflectionUtils.getPrivateField(ASTHolderCUInfo.class, "resolveBindings", astHolder);
reconcileFlags = (Integer) ReflectionUtils.getPrivateField(ASTHolderCUInfo.class, "reconcileFlags", astHolder);
problems = HashMap.class.cast(ReflectionUtils.getPrivateField(ASTHolderCUInfo.class, "problems", astHolder));
} else {
createAST = false;
resolveBindings = false;
reconcileFlags = 0;
problems = null;
}
boolean computeProblems = perWorkingCopyInfo != null && perWorkingCopyInfo.isActive() && project != null && JavaProject.hasJavaNature(project.getProject());
// compiler options
Map<String, String> options = (project == null ? JavaCore.getOptions() : project.getOptions(true));
if (!computeProblems) {
// disable task tags checking to speed up parsing
options.put(JavaCore.COMPILER_TASK_TAGS, "");
}
options.put(CompilerOptions.OPTIONG_BuildGroovyFiles, CompilerOptions.ENABLED);
CompilerOptions compilerOptions = new CompilerOptions(options);
if (project != null) {
CompilerUtils.setGroovyClasspath(compilerOptions, project);
}
ProblemReporter reporter = new ProblemReporter(new GroovyErrorHandlingPolicy(!computeProblems), compilerOptions, new DefaultProblemFactory());
SourceElementParser parser = new MultiplexingSourceElementRequestorParser(
reporter,
requestor, // not needed if computing groovy only
reporter.problemFactory,
compilerOptions,
true, // report local declarations
!createAST // optimize string literals only if not creating a DOM AST
);
parser.reportOnlyOneSyntaxError = !computeProblems;
// maybe not needed for groovy, but I don't want to find out.
parser.setMethodsFullRecovery(true);
parser.setStatementsRecovery((reconcileFlags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0);
if (!computeProblems && !resolveBindings && !createAST) // disable javadoc parsing if not computing problems, not resolving and not creating ast
parser.javadocParser.checkDocComment = false;
requestor.setParser(parser);
// update timestamp (might be IResource.NULL_STAMP if original does not exist)
if (underlyingResource == null) {
underlyingResource = getResource();
}
// underlying resource is null in the case of a working copy on a class file in a jar
if (underlyingResource != null) {
ReflectionUtils.setPrivateField(CompilationUnitElementInfo.class, "timestamp", info, underlyingResource.getModificationStamp());
}
GroovyCompilationUnitDeclaration compilationUnitDeclaration = null;
CompilationUnit source = cloneCachingContents();
try {
// GROOVY
// note that this is a slightly different approach than taken by super.buildStructure
// in super.buildStructure, there is a test here to see if computeProblems is true.
// if false, then parser.parserCompilationUnit is called.
// this will not work for Groovy because we need to ensure bindings are resolved
// for many operations (content assist and code select) to work.
// So, for groovy, always use CompilationUnitProblemFinder.process and then process problems
// separately only if necessary
// addendum (GRECLIPSE-942). The testcase for that bug includes a package with 200
// types in that refer to each other in a chain, through field references. If a reconcile
// references the top of the chain we can go through a massive number of recursive calls into
// this buildStructure for each one. The 'full' parse (with bindings) is only required for
// the top most (regardless of the computeProblems setting) and so we track how many recursive
// calls we have made - if we are at depth 2 we do what JDT was going to do (the quick thing).
if (computeProblems || depth.intValue() < 2) {
if (problems == null) {
// report problems to the problem requestor
problems = new HashMap<String, CategorizedProblem[]>();
compilationUnitDeclaration =
(GroovyCompilationUnitDeclaration) CompilationUnitProblemFinder.process(
source,
parser,
this.owner,
problems,
createAST,
reconcileFlags,
pm);
if (computeProblems) {
try {
perWorkingCopyInfo.beginReporting();
for (Iterator<CategorizedProblem[]> iteraror = problems.values().iterator(); iteraror.hasNext();) {
CategorizedProblem[] categorizedProblems = iteraror.next();
if (categorizedProblems == null)
continue;
for (int i = 0, n = categorizedProblems.length; i < n; i += 1) {
perWorkingCopyInfo.acceptProblem(categorizedProblems[i]);
}
}
} finally {
perWorkingCopyInfo.endReporting();
}
}
} else {
compilationUnitDeclaration =
(GroovyCompilationUnitDeclaration) CompilationUnitProblemFinder.process(
source,
parser,
this.owner,
problems,
createAST,
reconcileFlags,
pm);
}
} else {
compilationUnitDeclaration =
(GroovyCompilationUnitDeclaration) parser.parseCompilationUnit(
source,
true /* full parse to find local elements */,
pm);
}
// GROOVY
// if this is a working copy, then we have more work to do
maybeCacheModuleNode(perWorkingCopyInfo, compilationUnitDeclaration);
// create the DOM AST from the compiler AST
if (createAST) {
org.eclipse.jdt.core.dom.CompilationUnit ast;
try {
ast = AST.convertCompilationUnit(JavaConstants.AST_LEVEL, compilationUnitDeclaration, options, computeProblems, source, reconcileFlags, pm);
ReflectionUtils.setPrivateField(ASTHolderCUInfo.class, "ast", info, ast);
} catch (OperationCanceledException e) {
// catch this exception so as to not enter the catch(RuntimeException e) below
// might need to do the same for AbortCompilation
throw e;
} catch (IllegalArgumentException e) {
// if necessary, we can do some better reporting here.
Util.log(e, "Problem with build structure: Offset for AST node is incorrect in " + this.getParent().getElementName() + "." + getElementName());
} catch (Exception e) {
Util.log(e, "Problem with build structure for " + this.getElementName());
}
}
} catch (OperationCanceledException e) {
// catch this exception so as to not enter the catch(RuntimeException e) below
// might need to do the same for AbortCompilation
throw e;
} catch (JavaModelException e) {
// GRECLIPSE-1480 don't log element does not exist exceptions. since this could occur when element is in a non-java
// project
if (e.getStatus().getCode() != IJavaModelStatusConstants.ELEMENT_DOES_NOT_EXIST || this.getJavaProject().exists()) {
Util.log(e, "Problem with build structure for " + this.getElementName());
}
} catch (Exception e) {
// GROOVY: The groovy compiler does not handle broken code well in many situations
// use this general catch clause so that exceptions thrown by broken code
// do not bubble up the stack.
Util.log(e, "Problem with build structure for " + this.getElementName());
} finally {
if (compilationUnitDeclaration != null) {
compilationUnitDeclaration.cleanUp();
}
}
return info.isStructureKnown();
} finally {
depth.decrement();
if (GroovyLogManager.manager.hasLoggers()) {
GroovyLogManager.manager.logEnd("Build structure: " + name + " : " + Thread.currentThread().getName(), TraceCategory.COMPILER);
}
}
}
protected void maybeCacheModuleNode(JavaModelManager.PerWorkingCopyInfo perWorkingCopyInfo, GroovyCompilationUnitDeclaration compilationUnitDeclaration) {
ModuleNodeMapper.getInstance().maybeCacheModuleNode(perWorkingCopyInfo, compilationUnitDeclaration);
}
/*
* Copied from super class, but changed so that a custom ReconcileWorkingCopyOperation can be run
*/
@Override
public org.eclipse.jdt.core.dom.CompilationUnit reconcile(int astLevel, int reconcileFlags, WorkingCopyOwner workingCopyOwner, IProgressMonitor monitor)
throws JavaModelException {
if (!isWorkingCopy())
return null; // reconciling is not supported on non-working copies
if (workingCopyOwner == null)
workingCopyOwner = DefaultWorkingCopyOwner.PRIMARY;
PerformanceStats stats = null;
if (ReconcileWorkingCopyOperation.PERF) {
stats = PerformanceStats.getStats(JavaModelManager.RECONCILE_PERF, this);
stats.startRun(String.valueOf(getFileName()));
}
ReconcileWorkingCopyOperation op = new GroovyReconcileWorkingCopyOperation(this, astLevel, reconcileFlags, workingCopyOwner);
JavaModelManager manager = JavaModelManager.getJavaModelManager();
try {
manager.cacheZipFiles(this); // cache zip files for performance (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=134172)
op.runOperation(monitor);
} finally {
manager.flushZipFiles(this);
}
if (ReconcileWorkingCopyOperation.PERF) {
stats.endRun();
}
return op.ast;
}
@Override @SuppressWarnings({"rawtypes", "unchecked"})
public Object getAdapter(Class adapter) {
if (adapter == GroovyCompilationUnit.class) {
return this;
}
if (adapter == ModuleNode.class) {
return getModuleNode();
}
return super.getAdapter(adapter);
}
class CompilationUnitClone extends GroovyCompilationUnit {
private char[] cachedContents;
public CompilationUnitClone(char[] cachedContents) {
this();
this.cachedContents = cachedContents;
}
public CompilationUnitClone() {
super((PackageFragment) GroovyCompilationUnit.this.parent, GroovyCompilationUnit.this.name, GroovyCompilationUnit.this.owner);
}
@Override
public char[] getContents() {
if (this.cachedContents == null)
this.cachedContents = GroovyCompilationUnit.this.getContents();
return this.cachedContents;
}
@Override
public CompilationUnit originalFromClone() {
return GroovyCompilationUnit.this;
}
@Override
public char[] getFileName() {
return GroovyCompilationUnit.this.getFileName();
}
}
public GroovyCompilationUnit cloneCachingContents(char[] newContents) {
return new CompilationUnitClone(newContents);
}
/*
* Clone this handle so that it caches its contents in memory. DO NOT PASS TO CLIENTS
*/
@Override
public GroovyCompilationUnit cloneCachingContents() {
return new CompilationUnitClone();
}
@Override
public IJavaElement[] codeSelect(int offset, int length)
throws JavaModelException {
return codeSelect(offset, length, DefaultWorkingCopyOwner.PRIMARY);
}
@Override
public IJavaElement[] codeSelect(int offset, int length, WorkingCopyOwner workingCopyOwner)
throws JavaModelException {
return codeSelect(this, offset, length, workingCopyOwner);
}
@Override
protected IJavaElement[] codeSelect(org.eclipse.jdt.internal.compiler.env.ICompilationUnit cu, int offset, int length, WorkingCopyOwner o)
throws JavaModelException {
if (CodeSelectHelperFactory.selectHelper != null) {
return CodeSelectHelperFactory.selectHelper.select(this, offset, length);
}
return new IJavaElement[0];
}
/**
* There is no such thing as a primary type in Groovy. First look for a type of the same name as the CU, Else get the first type
* in getAllTypes()
*/
@Override
public IType findPrimaryType() {
IType type = super.findPrimaryType();
if (type != null) {
return type;
}
try {
if (getResource().exists()) {
IType[] types = getTypes();
if (types != null && types.length > 0) {
return types[0];
}
}
} catch (JavaModelException e) {
// can ignore situations when trying to find types that are not on the classpath
if (e.getStatus().getCode() != IJavaModelStatusConstants.ELEMENT_NOT_ON_CLASSPATH) {
Util.log(e, "Error finding all types of " + this.getElementName());
}
}
return null;
}
public boolean isOnBuildPath() {
// fix for bug http://dev.eclipse.org/bugs/show_bug.cgi?id=20051
IJavaProject project = this.getJavaProject();
if (!project.isOnClasspath(this)) {
return false;
}
IProject resourceProject = project.getProject();
if (resourceProject == null || !resourceProject.isAccessible() || !GroovyNature.hasGroovyNature(resourceProject)) {
return false;
}
return true;
}
@Override
public IResource getUnderlyingResource() throws JavaModelException {
if (isOnBuildPath()) {
return super.getUnderlyingResource();
} else {
// Super class method appears to only work correctly when we are on the build
// path. Otherwise parent, which is a package, is seen as non-existent.
// This causes a JavaModel exception.
IResource rsrc = getResource();
// I think that getResource *should* just return a path to the .groovy file
// under these circumstances.
try {
// What we did is rather hacky, double check result with this assert:
Assert.isTrue(rsrc.getFullPath().toString().endsWith(name));
} catch (Throwable e) {
Util.log(e);
return super.getUnderlyingResource();
}
return rsrc;
}
}
@Override
public void rename(String newName, boolean force, IProgressMonitor monitor) throws JavaModelException {
super.rename(newName, force, monitor);
// FIXADE we should not have to do this. Somewhere, a working copy is being created and not discarded
if (this.isWorkingCopy()) {
this.discardWorkingCopy();
}
}
protected void codeComplete(org.eclipse.jdt.internal.compiler.env.ICompilationUnit cu,
org.eclipse.jdt.internal.compiler.env.ICompilationUnit unitToSkip, int position, CompletionRequestor requestor,
WorkingCopyOwner owner, ITypeRoot typeRoot, IProgressMonitor monitor) throws JavaModelException {
// allow a delegate to perform completion if required
// this is used by the grails plugin when editing in gsp editor
ICodeCompletionDelegate delegate = (ICodeCompletionDelegate) getAdapter(ICodeCompletionDelegate.class);
if (delegate != null && delegate.shouldCodeComplete(requestor, typeRoot)) {
delegate.codeComplete(cu, unitToSkip, position, requestor, owner, typeRoot, monitor);
} else {
super.codeComplete(cu, unitToSkip, position, requestor, owner, typeRoot, monitor);
}
}
}