1/*
2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.jshell;
27
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.LinkedHashSet;
32import java.util.List;
33import java.util.Set;
34import java.util.stream.Stream;
35import jdk.jshell.ClassTracker.ClassInfo;
36import jdk.jshell.Snippet.Kind;
37import jdk.jshell.Snippet.Status;
38import jdk.jshell.Snippet.SubKind;
39import jdk.jshell.TaskFactory.AnalyzeTask;
40import jdk.jshell.TaskFactory.CompileTask;
41import jdk.jshell.spi.ExecutionControl.ClassBytecodes;
42import jdk.jshell.spi.ExecutionControl.ClassInstallException;
43import jdk.jshell.spi.ExecutionControl.EngineTerminationException;
44import jdk.jshell.spi.ExecutionControl.NotImplementedException;
45import static java.util.stream.Collectors.toList;
46import static java.util.stream.Collectors.toSet;
47import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
48import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
49import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP;
50import static jdk.jshell.Snippet.Status.OVERWRITTEN;
51import static jdk.jshell.Snippet.Status.RECOVERABLE_DEFINED;
52import static jdk.jshell.Snippet.Status.RECOVERABLE_NOT_DEFINED;
53import static jdk.jshell.Snippet.Status.REJECTED;
54import static jdk.jshell.Snippet.Status.VALID;
55import static jdk.jshell.Util.PARSED_LOCALE;
56import static jdk.jshell.Util.expunge;
57
58/**
59 * Tracks the compilation and load of a new or updated snippet.
60 * @author Robert Field
61 */
62final class Unit {
63
64    private final JShell state;
65    private final Snippet si;
66    private final Snippet siOld;
67    private final boolean isDependency;
68    private final boolean isNew;
69    private final Snippet causalSnippet;
70    private final DiagList generatedDiagnostics;
71
72    private int seq;
73    private String classNameInitial;
74    private Wrap activeGuts;
75    private Status status;
76    private Status prevStatus;
77    private boolean signatureChanged;
78    private DiagList compilationDiagnostics;
79    private DiagList recompilationDiagnostics = null;
80    private List<String> unresolved;
81    private SnippetEvent replaceOldEvent;
82    private List<SnippetEvent> secondaryEvents;
83    private boolean isAttemptingCorral;
84    private List<ClassInfo> toRedefine;
85    private boolean dependenciesNeeded;
86
87    Unit(JShell state, Snippet si, Snippet causalSnippet,
88            DiagList generatedDiagnostics) {
89        this.state = state;
90        this.si = si;
91        this.isDependency = causalSnippet != null;
92        this.siOld = isDependency
93                ? si
94                : state.maps.getSnippet(si.key());
95        this.isNew = siOld == null;
96        this.causalSnippet = causalSnippet;
97        this.generatedDiagnostics = generatedDiagnostics;
98
99        this.seq = isNew? 0 : siOld.sequenceNumber();
100        this.classNameInitial = isNew? "<none>" : siOld.className();
101        this.prevStatus = (isNew || isDependency)
102                ? si.status()
103                : siOld.status();
104        si.setSequenceNumber(seq);
105    }
106
107    // Drop entry
108    Unit(JShell state, Snippet si) {
109        this.state = state;
110        this.si = si;
111        this.siOld = null;
112        this.isDependency = false;
113        this.isNew = false;
114        this.causalSnippet = null;
115        this.generatedDiagnostics = new DiagList();
116        this.prevStatus = si.status();
117        si.setDropped();
118        this.status = si.status();
119    }
120
121    @Override
122    public int hashCode() {
123        return si.hashCode();
124    }
125
126    @Override
127    public boolean equals(Object o) {
128        return (o instanceof Unit)
129                ? si.equals(((Unit) o).si)
130                : false;
131    }
132
133    Snippet snippet() {
134        return si;
135    }
136
137    boolean isDependency() {
138        return isDependency;
139    }
140
141    void initialize() {
142        isAttemptingCorral = false;
143        dependenciesNeeded = false;
144        toRedefine = null; // assure NPE if classToLoad not called
145        activeGuts = si.guts();
146        markOldDeclarationOverwritten();
147    }
148
149    // Set the outer wrap of our Snippet
150    void setWrap(Collection<Unit> exceptUnit, Collection<Unit> plusUnfiltered) {
151        if (isImport()) {
152            si.setOuterWrap(state.outerMap.wrapImport(activeGuts, si));
153        } else {
154            // Collect Units for be wrapped together.  Just this except for overloaded methods
155            List<Unit> units;
156            if (snippet().kind() == Kind.METHOD) {
157                String name = ((MethodSnippet) snippet()).name();
158                units = plusUnfiltered.stream()
159                        .filter(u -> u.snippet().kind() == Kind.METHOD &&
160                                 ((MethodSnippet) u.snippet()).name().equals(name))
161                        .collect(toList());
162            } else {
163                units = Collections.singletonList(this);
164            }
165            // Keys to exclude from imports
166            Set<Key> except = exceptUnit.stream()
167                    .map(u -> u.snippet().key())
168                    .collect(toSet());
169            // Snippets to add to imports
170            Collection<Snippet> plus = plusUnfiltered.stream()
171                    .filter(u -> !units.contains(u))
172                    .map(Unit::snippet)
173                    .collect(toList());
174            // Snippets to wrap in an outer
175            List<Snippet> snippets = units.stream()
176                    .map(Unit::snippet)
177                    .collect(toList());
178            // Snippet wraps to wrap in an outer
179            List<Wrap> wraps = units.stream()
180                    .map(u -> u.activeGuts)
181                    .collect(toList());
182            // Set the outer wrap for this snippet
183            si.setOuterWrap(state.outerMap.wrapInClass(except, plus, snippets, wraps));
184            state.debug(DBG_WRAP, "++setWrap() %s\n%s\n",
185                    si, si.outerWrap().wrapped());
186        }
187    }
188
189    void setDiagnostics(AnalyzeTask ct) {
190        setDiagnostics(ct.getDiagnostics().ofUnit(this));
191    }
192
193    void setDiagnostics(DiagList diags) {
194        compilationDiagnostics = diags;
195        UnresolvedExtractor ue = new UnresolvedExtractor(diags);
196        unresolved = ue.unresolved();
197        state.debug(DBG_GEN, "++setCompilationInfo() %s\n%s\n-- diags: %s\n",
198                si, si.outerWrap().wrapped(), diags);
199    }
200
201    private boolean isRecoverable() {
202        // Unit failed, use corralling if it is defined on this Snippet,
203        // and either all the errors are resolution errors or this is a
204        // redeclare of an existing method
205        return compilationDiagnostics.hasErrors()
206                && si instanceof DeclarationSnippet
207                && (isDependency()
208                    || (si.subKind() != SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND
209                        && compilationDiagnostics.hasResolutionErrorsAndNoOthers()));
210    }
211
212    /**
213     * If it meets the conditions for corralling, install the corralled wrap
214     * @return true is the corralled wrap was installed
215     */
216    boolean corralIfNeeded(Collection<Unit> working) {
217        if (isRecoverable()
218                && si.corralled() != null) {
219            activeGuts = si.corralled();
220            setWrap(working, working);
221            return isAttemptingCorral = true;
222        }
223        return isAttemptingCorral = false;
224    }
225
226    void setCorralledDiagnostics(AnalyzeTask cct) {
227        // set corralled diagnostics, but don't reset unresolved
228        recompilationDiagnostics = cct.getDiagnostics().ofUnit(this);
229        state.debug(DBG_GEN, "++recomp %s\n%s\n-- diags: %s\n",
230                si, si.outerWrap().wrapped(), recompilationDiagnostics);
231    }
232
233    boolean smashingErrorDiagnostics(CompileTask ct) {
234        if (isDefined()) {
235            // set corralled diagnostics, but don't reset unresolved
236            DiagList dl = ct.getDiagnostics().ofUnit(this);
237            if (dl.hasErrors()) {
238                setDiagnostics(dl);
239                status = RECOVERABLE_NOT_DEFINED;
240                // overwrite orginal bytes
241                state.debug(DBG_GEN, "++smashingErrorDiagnostics %s\n%s\n-- diags: %s\n",
242                        si, si.outerWrap().wrapped(), dl);
243                return true;
244            }
245        }
246        return false;
247    }
248
249    void setStatus(AnalyzeTask at) {
250        if (!compilationDiagnostics.hasErrors()) {
251            status = VALID;
252        } else if (isRecoverable()) {
253            if (isAttemptingCorral && !recompilationDiagnostics.hasErrors()) {
254                status = RECOVERABLE_DEFINED;
255            } else {
256                status = RECOVERABLE_NOT_DEFINED;
257            }
258        } else {
259            status = REJECTED;
260        }
261        checkForOverwrite(at);
262
263        state.debug(DBG_GEN, "setStatus() %s - status: %s\n",
264                si, status);
265    }
266
267    boolean isDefined() {
268        return status.isDefined();
269    }
270
271    /**
272     * Process the class information from the last compile. Requires loading of
273     * returned list.
274     *
275     * @return the list of classes to load
276     */
277    Stream<ClassBytecodes> classesToLoad(List<String> classnames) {
278        toRedefine = new ArrayList<>();
279        List<ClassBytecodes> toLoad = new ArrayList<>();
280        if (status.isDefined() && !isImport()) {
281            // Classes should only be loaded/redefined if the compile left them
282            // in a defined state.  Imports do not have code and are not loaded.
283            for (String cn : classnames) {
284                ClassInfo ci = state.classTracker.get(cn);
285                if (ci.isLoaded()) {
286                    if (ci.isCurrent()) {
287                        // nothing to do
288                    } else {
289                        toRedefine.add(ci);
290                    }
291                } else {
292                    // If not loaded, add to the list of classes to load.
293                    toLoad.add(ci.toClassBytecodes());
294                    dependenciesNeeded = true;
295                }
296            }
297        }
298        return toLoad.stream();
299    }
300
301    /**
302     * Redefine classes needing redefine. classesToLoad() must be called first.
303     *
304     * @return true if all redefines succeeded (can be vacuously true)
305     */
306    boolean doRedefines() {
307        if (toRedefine.isEmpty()) {
308            return true;
309        }
310        ClassBytecodes[] cbcs = toRedefine.stream()
311                .map(ClassInfo::toClassBytecodes)
312                .toArray(ClassBytecodes[]::new);
313        try {
314            state.executionControl().redefine(cbcs);
315            state.classTracker.markLoaded(cbcs);
316            return true;
317        } catch (ClassInstallException ex) {
318            state.classTracker.markLoaded(cbcs, ex.installed());
319            return false;
320        } catch (EngineTerminationException ex) {
321            state.closeDown();
322            return false;
323        } catch (NotImplementedException ex) {
324            return false;
325        }
326    }
327
328    void markForReplacement() {
329        // increment for replace class wrapper
330        si.setSequenceNumber(++seq);
331    }
332
333    private boolean isImport() {
334        return si.kind() == Kind.IMPORT;
335    }
336
337    private boolean sigChanged() {
338        return (status.isDefined() != prevStatus.isDefined())
339                || (status.isDefined() && !si.className().equals(classNameInitial))
340                || signatureChanged;
341    }
342
343    Stream<Unit> effectedDependents() {
344        //System.err.printf("effectedDependents sigChanged=%b  dependenciesNeeded=%b   status=%s\n",
345        //       sigChanged(), dependenciesNeeded, status);
346        return sigChanged() || dependenciesNeeded || status == RECOVERABLE_NOT_DEFINED
347                ? dependents()
348                : Stream.empty();
349    }
350
351    Stream<Unit> dependents() {
352        return state.maps.getDependents(si)
353                    .stream()
354                    .filter(xsi -> xsi != si && xsi.status().isActive())
355                    .map(xsi -> new Unit(state, xsi, si, new DiagList()));
356    }
357
358    void finish() {
359        recordCompilation();
360        state.maps.installSnippet(si);
361    }
362
363    private void markOldDeclarationOverwritten() {
364        if (si != siOld && siOld != null && siOld.status().isActive()) {
365            // Mark the old declaraion as replaced
366            replaceOldEvent = new SnippetEvent(siOld,
367                    siOld.status(), OVERWRITTEN,
368                    false, si, null, null);
369            siOld.setOverwritten();
370        }
371    }
372
373    private DiagList computeDiagnostics() {
374        DiagList diagnostics = new DiagList();
375        DiagList diags = compilationDiagnostics;
376        if (status == RECOVERABLE_DEFINED || status == RECOVERABLE_NOT_DEFINED) {
377            UnresolvedExtractor ue = new UnresolvedExtractor(diags);
378            diagnostics.addAll(ue.otherAll());
379        } else {
380            unresolved = Collections.emptyList();
381            diagnostics.addAll(diags);
382        }
383        diagnostics.addAll(generatedDiagnostics);
384        return diagnostics;
385    }
386
387    private void recordCompilation() {
388        state.maps.mapDependencies(si);
389        DiagList diags = computeDiagnostics();
390        si.setCompilationStatus(status, unresolved, diags);
391        state.debug(DBG_GEN, "recordCompilation: %s -- status %s, unresolved %s\n",
392                si, status, unresolved);
393    }
394
395    private void checkForOverwrite(AnalyzeTask at) {
396        secondaryEvents = new ArrayList<>();
397        if (replaceOldEvent != null) secondaryEvents.add(replaceOldEvent);
398
399        // Defined methods can overwrite methods of other (equivalent) snippets
400        if (isNew && si.kind() == Kind.METHOD && status.isDefined()) {
401            MethodSnippet msi = (MethodSnippet)si;
402            String oqpt = msi.qualifiedParameterTypes();
403            String nqpt = computeQualifiedParameterTypes(at, msi);
404            if (!nqpt.equals(oqpt)) {
405                msi.setQualifiedParamaterTypes(nqpt);
406                Status overwrittenStatus = overwriteMatchingMethod(msi);
407                if (overwrittenStatus != null) {
408                    prevStatus = overwrittenStatus;
409                    signatureChanged = true;
410                }
411            }
412        }
413    }
414
415    // Check if there is a method whose user-declared parameter types are
416    // different (and thus has a different snippet) but whose compiled parameter
417    // types are the same. if so, consider it an overwrite replacement.
418    private Status overwriteMatchingMethod(MethodSnippet msi) {
419        String qpt = msi.qualifiedParameterTypes();
420        List<MethodSnippet> matching = state.methods()
421                .filter(sn ->
422                           sn != null
423                        && sn != msi
424                        && sn.status().isActive()
425                        && sn.name().equals(msi.name())
426                        && qpt.equals(sn.qualifiedParameterTypes()))
427                .collect(toList());
428
429        // Look through all methods for a method of the same name, with the
430        // same computed qualified parameter types
431        Status overwrittenStatus = null;
432        for (MethodSnippet sn : matching) {
433            overwrittenStatus = sn.status();
434            SnippetEvent se = new SnippetEvent(
435                    sn, overwrittenStatus, OVERWRITTEN,
436                    false, msi, null, null);
437            sn.setOverwritten();
438            secondaryEvents.add(se);
439            state.debug(DBG_EVNT,
440                    "Overwrite event #%d -- key: %s before: %s status: %s sig: %b cause: %s\n",
441                    secondaryEvents.size(), se.snippet(), se.previousStatus(),
442                    se.status(), se.isSignatureChange(), se.causeSnippet());
443        }
444        return overwrittenStatus;
445    }
446
447    private String computeQualifiedParameterTypes(AnalyzeTask at, MethodSnippet msi) {
448        String rawSig = TreeDissector.createBySnippet(at, msi).typeOfMethod(msi);
449        String signature = expunge(rawSig);
450        int paren = signature.lastIndexOf(')');
451
452        // Extract the parameter type string from the method signature,
453        // if method did not compile use the user-supplied parameter types
454        return paren >= 0
455                ? signature.substring(0, paren + 1)
456                : msi.parameterTypes();
457    }
458
459    SnippetEvent event(String value, JShellException exception) {
460        boolean wasSignatureChanged = sigChanged();
461        state.debug(DBG_EVNT, "Snippet: %s id: %s before: %s status: %s sig: %b cause: %s\n",
462                si, si.id(), prevStatus, si.status(), wasSignatureChanged, causalSnippet);
463        return new SnippetEvent(si, prevStatus, si.status(),
464                wasSignatureChanged, causalSnippet, value, exception);
465    }
466
467    List<SnippetEvent> secondaryEvents() {
468        return secondaryEvents==null
469                ? Collections.emptyList()
470                : secondaryEvents;
471    }
472
473    @Override
474    public String toString() {
475        return "Unit(" + si.name() + ")";
476    }
477
478    /**
479     * Separate out the unresolvedDependencies errors from both the other
480     * corralling errors and the overall errors.
481     */
482    private static class UnresolvedExtractor {
483
484        private static final String RESOLVE_ERROR_SYMBOL = "symbol:";
485        private static final String RESOLVE_ERROR_LOCATION = "location:";
486
487        //TODO extract from tree instead -- note: internationalization
488        private final Set<String> unresolved = new LinkedHashSet<>();
489        private final DiagList otherErrors = new DiagList();
490        private final DiagList otherAll = new DiagList();
491
492        UnresolvedExtractor(DiagList diags) {
493            for (Diag diag : diags) {
494                if (diag.isError()) {
495                    if (diag.isResolutionError()) {
496                        String m = diag.getMessage(PARSED_LOCALE);
497                        int symPos = m.indexOf(RESOLVE_ERROR_SYMBOL);
498                        if (symPos >= 0) {
499                            m = m.substring(symPos + RESOLVE_ERROR_SYMBOL.length());
500                            int symLoc = m.indexOf(RESOLVE_ERROR_LOCATION);
501                            if (symLoc >= 0) {
502                                m = m.substring(0, symLoc);
503                            }
504                            m = m.trim();
505                            unresolved.add(m);
506                            continue;
507                        }
508                    }
509                    otherErrors.add(diag);
510                }
511                otherAll.add(diag);
512            }
513        }
514
515        DiagList otherCorralledErrors() {
516            return otherErrors;
517        }
518
519        DiagList otherAll() {
520            return otherAll;
521        }
522
523        List<String> unresolved() {
524            return new ArrayList<>(unresolved);
525        }
526    }
527}
528