/*
* Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jshell;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import jdk.jshell.ClassTracker.ClassInfo;
import jdk.jshell.Snippet.Kind;
import jdk.jshell.Snippet.Status;
import jdk.jshell.Snippet.SubKind;
import jdk.jshell.TaskFactory.AnalyzeTask;
import jdk.jshell.TaskFactory.CompileTask;
import jdk.jshell.spi.ExecutionControl.ClassBytecodes;
import jdk.jshell.spi.ExecutionControl.ClassInstallException;
import jdk.jshell.spi.ExecutionControl.EngineTerminationException;
import jdk.jshell.spi.ExecutionControl.NotImplementedException;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP;
import static jdk.jshell.Snippet.Status.OVERWRITTEN;
import static jdk.jshell.Snippet.Status.RECOVERABLE_DEFINED;
import static jdk.jshell.Snippet.Status.RECOVERABLE_NOT_DEFINED;
import static jdk.jshell.Snippet.Status.REJECTED;
import static jdk.jshell.Snippet.Status.VALID;
import static jdk.jshell.Util.PARSED_LOCALE;
import static jdk.jshell.Util.expunge;
/**
* Tracks the compilation and load of a new or updated snippet.
* @author Robert Field
*/
final class Unit {
private final JShell state;
private final Snippet si;
private final Snippet siOld;
private final boolean isDependency;
private final boolean isNew;
private final Snippet causalSnippet;
private final DiagList generatedDiagnostics;
private int seq;
private String classNameInitial;
private Wrap activeGuts;
private Status status;
private Status prevStatus;
private boolean signatureChanged;
private DiagList compilationDiagnostics;
private DiagList recompilationDiagnostics = null;
private List<String> unresolved;
private SnippetEvent replaceOldEvent;
private List<SnippetEvent> secondaryEvents;
private boolean isAttemptingCorral;
private List<ClassInfo> toRedefine;
private boolean dependenciesNeeded;
Unit(JShell state, Snippet si, Snippet causalSnippet,
DiagList generatedDiagnostics) {
this.state = state;
this.si = si;
this.isDependency = causalSnippet != null;
this.siOld = isDependency
? si
: state.maps.getSnippet(si.key());
this.isNew = siOld == null;
this.causalSnippet = causalSnippet;
this.generatedDiagnostics = generatedDiagnostics;
this.seq = isNew? 0 : siOld.sequenceNumber();
this.classNameInitial = isNew? "<none>" : siOld.className();
this.prevStatus = (isNew || isDependency)
? si.status()
: siOld.status();
si.setSequenceNumber(seq);
}
// Drop entry
Unit(JShell state, Snippet si) {
this.state = state;
this.si = si;
this.siOld = null;
this.isDependency = false;
this.isNew = false;
this.causalSnippet = null;
this.generatedDiagnostics = new DiagList();
this.prevStatus = si.status();
si.setDropped();
this.status = si.status();
}
@Override
public int hashCode() {
return si.hashCode();
}
@Override
public boolean equals(Object o) {
return (o instanceof Unit)
? si.equals(((Unit) o).si)
: false;
}
Snippet snippet() {
return si;
}
boolean isDependency() {
return isDependency;
}
void initialize() {
isAttemptingCorral = false;
dependenciesNeeded = false;
toRedefine = null; // assure NPE if classToLoad not called
activeGuts = si.guts();
markOldDeclarationOverwritten();
}
// Set the outer wrap of our Snippet
void setWrap(Collection<Unit> exceptUnit, Collection<Unit> plusUnfiltered) {
if (isImport()) {
si.setOuterWrap(state.outerMap.wrapImport(activeGuts, si));
} else {
// Collect Units for be wrapped together. Just this except for overloaded methods
List<Unit> units;
if (snippet().kind() == Kind.METHOD) {
String name = ((MethodSnippet) snippet()).name();
units = plusUnfiltered.stream()
.filter(u -> u.snippet().kind() == Kind.METHOD &&
((MethodSnippet) u.snippet()).name().equals(name))
.collect(toList());
} else {
units = Collections.singletonList(this);
}
// Keys to exclude from imports
Set<Key> except = exceptUnit.stream()
.map(u -> u.snippet().key())
.collect(toSet());
// Snippets to add to imports
Collection<Snippet> plus = plusUnfiltered.stream()
.filter(u -> !units.contains(u))
.map(Unit::snippet)
.collect(toList());
// Snippets to wrap in an outer
List<Snippet> snippets = units.stream()
.map(Unit::snippet)
.collect(toList());
// Snippet wraps to wrap in an outer
List<Wrap> wraps = units.stream()
.map(u -> u.activeGuts)
.collect(toList());
// Set the outer wrap for this snippet
si.setOuterWrap(state.outerMap.wrapInClass(except, plus, snippets, wraps));
state.debug(DBG_WRAP, "++setWrap() %s\n%s\n",
si, si.outerWrap().wrapped());
}
}
void setDiagnostics(AnalyzeTask ct) {
setDiagnostics(ct.getDiagnostics().ofUnit(this));
}
void setDiagnostics(DiagList diags) {
compilationDiagnostics = diags;
UnresolvedExtractor ue = new UnresolvedExtractor(diags);
unresolved = ue.unresolved();
state.debug(DBG_GEN, "++setCompilationInfo() %s\n%s\n-- diags: %s\n",
si, si.outerWrap().wrapped(), diags);
}
private boolean isRecoverable() {
// Unit failed, use corralling if it is defined on this Snippet,
// and either all the errors are resolution errors or this is a
// redeclare of an existing method
return compilationDiagnostics.hasErrors()
&& si instanceof DeclarationSnippet
&& (isDependency()
|| (si.subKind() != SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
&& compilationDiagnostics.hasResolutionErrorsAndNoOthers()));
}
/**
* If it meets the conditions for corralling, install the corralled wrap
* @return true is the corralled wrap was installed
*/
boolean corralIfNeeded(Collection<Unit> working) {
if (isRecoverable()
&& si.corralled() != null) {
activeGuts = si.corralled();
setWrap(working, working);
return isAttemptingCorral = true;
}
return isAttemptingCorral = false;
}
void setCorralledDiagnostics(AnalyzeTask cct) {
// set corralled diagnostics, but don't reset unresolved
recompilationDiagnostics = cct.getDiagnostics().ofUnit(this);
state.debug(DBG_GEN, "++recomp %s\n%s\n-- diags: %s\n",
si, si.outerWrap().wrapped(), recompilationDiagnostics);
}
boolean smashingErrorDiagnostics(CompileTask ct) {
if (isDefined()) {
// set corralled diagnostics, but don't reset unresolved
DiagList dl = ct.getDiagnostics().ofUnit(this);
if (dl.hasErrors()) {
setDiagnostics(dl);
status = RECOVERABLE_NOT_DEFINED;
// overwrite orginal bytes
state.debug(DBG_GEN, "++smashingErrorDiagnostics %s\n%s\n-- diags: %s\n",
si, si.outerWrap().wrapped(), dl);
return true;
}
}
return false;
}
void setStatus(AnalyzeTask at) {
if (!compilationDiagnostics.hasErrors()) {
status = VALID;
} else if (isRecoverable()) {
if (isAttemptingCorral && !recompilationDiagnostics.hasErrors()) {
status = RECOVERABLE_DEFINED;
} else {
status = RECOVERABLE_NOT_DEFINED;
}
} else {
status = REJECTED;
}
checkForOverwrite(at);
state.debug(DBG_GEN, "setStatus() %s - status: %s\n",
si, status);
}
boolean isDefined() {
return status.isDefined();
}
/**
* Process the class information from the last compile. Requires loading of
* returned list.
*
* @return the list of classes to load
*/
Stream<ClassBytecodes> classesToLoad(List<String> classnames) {
toRedefine = new ArrayList<>();
List<ClassBytecodes> toLoad = new ArrayList<>();
if (status.isDefined() && !isImport()) {
// Classes should only be loaded/redefined if the compile left them
// in a defined state. Imports do not have code and are not loaded.
for (String cn : classnames) {
ClassInfo ci = state.classTracker.get(cn);
if (ci.isLoaded()) {
if (ci.isCurrent()) {
// nothing to do
} else {
toRedefine.add(ci);
}
} else {
// If not loaded, add to the list of classes to load.
toLoad.add(ci.toClassBytecodes());
dependenciesNeeded = true;
}
}
}
return toLoad.stream();
}
/**
* Redefine classes needing redefine. classesToLoad() must be called first.
*
* @return true if all redefines succeeded (can be vacuously true)
*/
boolean doRedefines() {
if (toRedefine.isEmpty()) {
return true;
}
ClassBytecodes[] cbcs = toRedefine.stream()
.map(ClassInfo::toClassBytecodes)
.toArray(ClassBytecodes[]::new);
try {
state.executionControl().redefine(cbcs);
state.classTracker.markLoaded(cbcs);
return true;
} catch (ClassInstallException ex) {
state.classTracker.markLoaded(cbcs, ex.installed());
return false;
} catch (EngineTerminationException ex) {
state.closeDown();
return false;
} catch (NotImplementedException ex) {
return false;
}
}
void markForReplacement() {
// increment for replace class wrapper
si.setSequenceNumber(++seq);
}
private boolean isImport() {
return si.kind() == Kind.IMPORT;
}
private boolean sigChanged() {
return (status.isDefined() != prevStatus.isDefined())
|| (status.isDefined() && !si.className().equals(classNameInitial))
|| signatureChanged;
}
Stream<Unit> effectedDependents() {
//System.err.printf("effectedDependents sigChanged=%b dependenciesNeeded=%b status=%s\n",
// sigChanged(), dependenciesNeeded, status);
return sigChanged() || dependenciesNeeded || status == RECOVERABLE_NOT_DEFINED
? dependents()
: Stream.empty();
}
Stream<Unit> dependents() {
return state.maps.getDependents(si)
.stream()
.filter(xsi -> xsi != si && xsi.status().isActive())
.map(xsi -> new Unit(state, xsi, si, new DiagList()));
}
void finish() {
recordCompilation();
state.maps.installSnippet(si);
}
private void markOldDeclarationOverwritten() {
if (si != siOld && siOld != null && siOld.status().isActive()) {
// Mark the old declaraion as replaced
replaceOldEvent = new SnippetEvent(siOld,
siOld.status(), OVERWRITTEN,
false, si, null, null);
siOld.setOverwritten();
}
}
private DiagList computeDiagnostics() {
DiagList diagnostics = new DiagList();
DiagList diags = compilationDiagnostics;
if (status == RECOVERABLE_DEFINED || status == RECOVERABLE_NOT_DEFINED) {
UnresolvedExtractor ue = new UnresolvedExtractor(diags);
diagnostics.addAll(ue.otherAll());
} else {
unresolved = Collections.emptyList();
diagnostics.addAll(diags);
}
diagnostics.addAll(generatedDiagnostics);
return diagnostics;
}
private void recordCompilation() {
state.maps.mapDependencies(si);
DiagList diags = computeDiagnostics();
si.setCompilationStatus(status, unresolved, diags);
state.debug(DBG_GEN, "recordCompilation: %s -- status %s, unresolved %s\n",
si, status, unresolved);
}
private void checkForOverwrite(AnalyzeTask at) {
secondaryEvents = new ArrayList<>();
if (replaceOldEvent != null) secondaryEvents.add(replaceOldEvent);
// Defined methods can overwrite methods of other (equivalent) snippets
if (isNew && si.kind() == Kind.METHOD && status.isDefined()) {
MethodSnippet msi = (MethodSnippet)si;
String oqpt = msi.qualifiedParameterTypes();
String nqpt = computeQualifiedParameterTypes(at, msi);
if (!nqpt.equals(oqpt)) {
msi.setQualifiedParamaterTypes(nqpt);
Status overwrittenStatus = overwriteMatchingMethod(msi);
if (overwrittenStatus != null) {
prevStatus = overwrittenStatus;
signatureChanged = true;
}
}
}
}
// Check if there is a method whose user-declared parameter types are
// different (and thus has a different snippet) but whose compiled parameter
// types are the same. if so, consider it an overwrite replacement.
private Status overwriteMatchingMethod(MethodSnippet msi) {
String qpt = msi.qualifiedParameterTypes();
List<MethodSnippet> matching = state.methods()
.filter(sn ->
sn != null
&& sn != msi
&& sn.status().isActive()
&& sn.name().equals(msi.name())
&& qpt.equals(sn.qualifiedParameterTypes()))
.collect(toList());
// Look through all methods for a method of the same name, with the
// same computed qualified parameter types
Status overwrittenStatus = null;
for (MethodSnippet sn : matching) {
overwrittenStatus = sn.status();
SnippetEvent se = new SnippetEvent(
sn, overwrittenStatus, OVERWRITTEN,
false, msi, null, null);
sn.setOverwritten();
secondaryEvents.add(se);
state.debug(DBG_EVNT,
"Overwrite event #%d -- key: %s before: %s status: %s sig: %b cause: %s\n",
secondaryEvents.size(), se.snippet(), se.previousStatus(),
se.status(), se.isSignatureChange(), se.causeSnippet());
}
return overwrittenStatus;
}
private String computeQualifiedParameterTypes(AnalyzeTask at, MethodSnippet msi) {
String rawSig = TreeDissector.createBySnippet(at, msi).typeOfMethod(msi);
String signature = expunge(rawSig);
int paren = signature.lastIndexOf(')');
// Extract the parameter type string from the method signature,
// if method did not compile use the user-supplied parameter types
return paren >= 0
? signature.substring(0, paren + 1)
: msi.parameterTypes();
}
SnippetEvent event(String value, JShellException exception) {
boolean wasSignatureChanged = sigChanged();
state.debug(DBG_EVNT, "Snippet: %s id: %s before: %s status: %s sig: %b cause: %s\n",
si, si.id(), prevStatus, si.status(), wasSignatureChanged, causalSnippet);
return new SnippetEvent(si, prevStatus, si.status(),
wasSignatureChanged, causalSnippet, value, exception);
}
List<SnippetEvent> secondaryEvents() {
return secondaryEvents==null
? Collections.emptyList()
: secondaryEvents;
}
@Override
public String toString() {
return "Unit(" + si.name() + ")";
}
/**
* Separate out the unresolvedDependencies errors from both the other
* corralling errors and the overall errors.
*/
private static class UnresolvedExtractor {
private static final String RESOLVE_ERROR_SYMBOL = "symbol:";
private static final String RESOLVE_ERROR_LOCATION = "location:";
//TODO extract from tree instead -- note: internationalization
private final Set<String> unresolved = new LinkedHashSet<>();
private final DiagList otherErrors = new DiagList();
private final DiagList otherAll = new DiagList();
UnresolvedExtractor(DiagList diags) {
for (Diag diag : diags) {
if (diag.isError()) {
if (diag.isResolutionError()) {
String m = diag.getMessage(PARSED_LOCALE);
int symPos = m.indexOf(RESOLVE_ERROR_SYMBOL);
if (symPos >= 0) {
m = m.substring(symPos + RESOLVE_ERROR_SYMBOL.length());
int symLoc = m.indexOf(RESOLVE_ERROR_LOCATION);
if (symLoc >= 0) {
m = m.substring(0, symLoc);
}
m = m.trim();
unresolved.add(m);
continue;
}
}
otherErrors.add(diag);
}
otherAll.add(diag);
}
}
DiagList otherCorralledErrors() {
return otherErrors;
}
DiagList otherAll() {
return otherAll;
}
List<String> unresolved() {
return new ArrayList<>(unresolved);
}
}
}