Analyzer.java revision 3170:dc017a37aac5
1/*
2 * Copyright (c) 2013, 2014, 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 com.sun.tools.jdeps;
27
28import java.io.PrintStream;
29import java.util.Comparator;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Map;
34import java.util.Objects;
35import java.util.Set;
36import java.util.stream.Collectors;
37import java.util.stream.Stream;
38
39import com.sun.tools.classfile.Dependency.Location;
40
41/**
42 * Dependency Analyzer.
43 */
44public class Analyzer {
45    /**
46     * Type of the dependency analysis.  Appropriate level of data
47     * will be stored.
48     */
49    public enum Type {
50        SUMMARY,
51        PACKAGE,
52        CLASS,
53        VERBOSE
54    }
55
56    /**
57     * Filter to be applied when analyzing the dependencies from the given archives.
58     * Only the accepted dependencies are recorded.
59     */
60    interface Filter {
61        boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive);
62    }
63
64    protected final Type type;
65    protected final Filter filter;
66    protected final Map<Archive, ArchiveDeps> results = new HashMap<>();
67    protected final Map<Location, Archive> map = new HashMap<>();
68    private static final Archive NOT_FOUND
69        = new Archive(JdepsTask.getMessage("artifact.not.found"));
70
71    /**
72     * Constructs an Analyzer instance.
73     *
74     * @param type Type of the dependency analysis
75     * @param filter
76     */
77    public Analyzer(Type type, Filter filter) {
78        this.type = type;
79        this.filter = filter;
80    }
81
82    /**
83     * Performs the dependency analysis on the given archives.
84     */
85    public boolean run(List<Archive> archives) {
86        // build a map from Location to Archive
87        buildLocationArchiveMap(archives);
88
89        // traverse and analyze all dependencies
90        for (Archive archive : archives) {
91            ArchiveDeps deps = new ArchiveDeps(archive, type);
92            archive.visitDependences(deps);
93            results.put(archive, deps);
94        }
95        return true;
96    }
97
98    protected void buildLocationArchiveMap(List<Archive> archives) {
99        // build a map from Location to Archive
100        for (Archive archive: archives) {
101            for (Location l: archive.getClasses()) {
102                if (!map.containsKey(l)) {
103                    map.put(l, archive);
104                } else {
105                    // duplicated class warning?
106                }
107            }
108        }
109    }
110
111    public boolean hasDependences(Archive archive) {
112        if (results.containsKey(archive)) {
113            return results.get(archive).dependencies().size() > 0;
114        }
115        return false;
116    }
117
118    public Set<String> dependences(Archive source) {
119        ArchiveDeps result = results.get(source);
120        return result.dependencies().stream()
121                     .map(Dep::target)
122                     .collect(Collectors.toSet());
123    }
124
125    public interface Visitor {
126        /**
127         * Visits a recorded dependency from origin to target which can be
128         * a fully-qualified classname, a package name, a module or
129         * archive name depending on the Analyzer's type.
130         */
131        public void visitDependence(String origin, Archive originArchive,
132                                    String target, Archive targetArchive);
133    }
134
135    /**
136     * Visit the dependencies of the given source.
137     * If the requested level is SUMMARY, it will visit the required archives list.
138     */
139    public void visitDependences(Archive source, Visitor v, Type level) {
140        if (level == Type.SUMMARY) {
141            final ArchiveDeps result = results.get(source);
142            final Set<Archive> reqs = result.requires();
143            Stream<Archive> stream = reqs.stream();
144            if (reqs.isEmpty()) {
145                if (hasDependences(source)) {
146                    // If reqs.isEmpty() and we have dependences, then it means
147                    // that the dependences are from 'source' onto itself.
148                    stream = Stream.of(source);
149                }
150            }
151            stream.sorted(Comparator.comparing(Archive::getName))
152                  .forEach(archive -> {
153                      Profile profile = result.getTargetProfile(archive);
154                      v.visitDependence(source.getName(), source,
155                                        profile != null ? profile.profileName() : archive.getName(), archive);
156                  });
157        } else {
158            ArchiveDeps result = results.get(source);
159            if (level != type) {
160                // requesting different level of analysis
161                result = new ArchiveDeps(source, level);
162                source.visitDependences(result);
163            }
164            result.dependencies().stream()
165                  .sorted(Comparator.comparing(Dep::origin)
166                                    .thenComparing(Dep::target))
167                  .forEach(d -> v.visitDependence(d.origin(), d.originArchive(), d.target(), d.targetArchive()));
168        }
169    }
170
171    public void visitDependences(Archive source, Visitor v) {
172        visitDependences(source, v, type);
173    }
174
175    /**
176     * ArchiveDeps contains the dependencies for an Archive that can have one or
177     * more classes.
178     */
179    class ArchiveDeps implements Archive.Visitor {
180        protected final Archive archive;
181        protected final Set<Archive> requires;
182        protected final Set<Dep> deps;
183        protected final Type level;
184        private Profile profile;
185        ArchiveDeps(Archive archive, Type level) {
186            this.archive = archive;
187            this.deps = new HashSet<>();
188            this.requires = new HashSet<>();
189            this.level = level;
190        }
191
192        Set<Dep> dependencies() {
193            return deps;
194        }
195
196        Set<Archive> requires() {
197            return requires;
198        }
199
200        Profile getTargetProfile(Archive target) {
201            if (target instanceof Module) {
202                return Profile.getProfile((Module) target);
203            } else {
204                return null;
205            }
206        }
207
208        Archive findArchive(Location t) {
209            Archive target = archive.getClasses().contains(t) ? archive : map.get(t);
210            if (target == null) {
211                map.put(t, target = NOT_FOUND);
212            }
213            return target;
214        }
215
216        // return classname or package name depedning on the level
217        private String getLocationName(Location o) {
218            if (level == Type.CLASS || level == Type.VERBOSE) {
219                return o.getClassName();
220            } else {
221                String pkg = o.getPackageName();
222                return pkg.isEmpty() ? "<unnamed>" : pkg;
223            }
224        }
225
226        @Override
227        public void visit(Location o, Location t) {
228            Archive targetArchive = findArchive(t);
229            if (filter.accepts(o, archive, t, targetArchive)) {
230                addDep(o, t);
231                if (archive != targetArchive && !requires.contains(targetArchive)) {
232                    requires.add(targetArchive);
233                }
234            }
235            if (targetArchive instanceof Module) {
236                Profile p = Profile.getProfile(t.getPackageName());
237                if (profile == null || (p != null && p.compareTo(profile) > 0)) {
238                    profile = p;
239                }
240            }
241        }
242
243        private Dep curDep;
244        protected Dep addDep(Location o, Location t) {
245            String origin = getLocationName(o);
246            String target = getLocationName(t);
247            Archive targetArchive = findArchive(t);
248            if (curDep != null &&
249                    curDep.origin().equals(origin) &&
250                    curDep.originArchive() == archive &&
251                    curDep.target().equals(target) &&
252                    curDep.targetArchive() == targetArchive) {
253                return curDep;
254            }
255
256            Dep e = new Dep(origin, archive, target, targetArchive);
257            if (deps.contains(e)) {
258                for (Dep e1 : deps) {
259                    if (e.equals(e1)) {
260                        curDep = e1;
261                    }
262                }
263            } else {
264                deps.add(e);
265                curDep = e;
266            }
267            return curDep;
268        }
269    }
270
271    /*
272     * Class-level or package-level dependency
273     */
274    class Dep {
275        final String origin;
276        final Archive originArchive;
277        final String target;
278        final Archive targetArchive;
279
280        Dep(String origin, Archive originArchive, String target, Archive targetArchive) {
281            this.origin = origin;
282            this.originArchive = originArchive;
283            this.target = target;
284            this.targetArchive = targetArchive;
285        }
286
287        String origin() {
288            return origin;
289        }
290
291        Archive originArchive() {
292            return originArchive;
293        }
294
295        String target() {
296            return target;
297        }
298
299        Archive targetArchive() {
300            return targetArchive;
301        }
302
303        @Override
304        @SuppressWarnings("unchecked")
305        public boolean equals(Object o) {
306            if (o instanceof Dep) {
307                Dep d = (Dep) o;
308                return this.origin.equals(d.origin) &&
309                        this.originArchive == d.originArchive &&
310                        this.target.equals(d.target) &&
311                        this.targetArchive == d.targetArchive;
312            }
313            return false;
314        }
315
316        @Override
317        public int hashCode() {
318            int hash = 7;
319            hash = 67*hash + Objects.hashCode(this.origin)
320                           + Objects.hashCode(this.originArchive)
321                           + Objects.hashCode(this.target)
322                           + Objects.hashCode(this.targetArchive);
323            return hash;
324        }
325
326        public String toString() {
327            return String.format("%s (%s) -> %s (%s)%n",
328                    origin, originArchive.getName(),
329                    target, targetArchive.getName());
330        }
331    }
332
333    static Analyzer getExportedAPIsAnalyzer() {
334        return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.reexportsFilter, true);
335    }
336
337    static Analyzer getModuleAccessAnalyzer() {
338        return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.accessCheckFilter, false);
339    }
340
341    private static class ModuleAccessAnalyzer extends Analyzer {
342        private final boolean apionly;
343        ModuleAccessAnalyzer(Filter filter, boolean apionly) {
344            super(Type.VERBOSE, filter);
345            this.apionly = apionly;
346        }
347        /**
348         * Verify module access
349         */
350        public boolean run(List<Archive> archives) {
351            // build a map from Location to Archive
352            buildLocationArchiveMap(archives);
353
354            // traverse and analyze all dependencies
355            int count = 0;
356            for (Archive archive : archives) {
357                ArchiveDeps checker = new ArchiveDeps(archive, type);
358                archive.visitDependences(checker);
359                count += checker.dependencies().size();
360                // output if any error
361                Module m = (Module)archive;
362                printDependences(System.err, m, checker.dependencies());
363                results.put(archive, checker);
364            }
365            return count == 0;
366        }
367
368        private void printDependences(PrintStream out, Module m, Set<Dep> deps) {
369            if (deps.isEmpty())
370                return;
371
372            String msg = apionly ? "API reference:" : "inaccessible reference:";
373            deps.stream().sorted(Comparator.comparing(Dep::origin)
374                                           .thenComparing(Dep::target))
375                .forEach(d -> out.format("%s %s (%s) -> %s (%s)%n", msg,
376                                         d.origin(), d.originArchive().getName(),
377                                         d.target(), d.targetArchive().getName()));
378            if (apionly) {
379                out.format("Dependences missing re-exports=\"true\" attribute:%n");
380                deps.stream()
381                        .map(Dep::targetArchive)
382                        .map(Archive::getName)
383                        .distinct()
384                        .sorted()
385                        .forEach(d -> out.format("  %s -> %s%n", m.name(), d));
386            }
387        }
388
389        private static Module findModule(Archive archive) {
390            if (Module.class.isInstance(archive)) {
391                return (Module) archive;
392            } else {
393                return null;
394            }
395        }
396
397        // returns true if target is accessible by origin
398        private static boolean canAccess(Location o, Archive originArchive, Location t, Archive targetArchive) {
399            Module origin = findModule(originArchive);
400            Module target = findModule(targetArchive);
401
402            if (targetArchive == Analyzer.NOT_FOUND) {
403                return false;
404            }
405
406            // unnamed module
407            // ## should check public type?
408            if (target == null)
409                return true;
410
411            // module-private
412            if (origin == target)
413                return true;
414
415            return target.isAccessibleTo(t.getClassName(), origin);
416        }
417
418        static final Filter accessCheckFilter = new Filter() {
419            @Override
420            public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) {
421                return !canAccess(o, originArchive, t, targetArchive);
422            }
423        };
424
425        static final Filter reexportsFilter = new Filter() {
426            @Override
427            public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) {
428                Module origin = findModule(originArchive);
429                Module target = findModule(targetArchive);
430                if (!origin.isExportedPackage(o.getPackageName())) {
431                    // filter non-exported classes
432                    return false;
433                }
434
435                boolean accessible = canAccess(o, originArchive, t, targetArchive);
436                if (!accessible)
437                    return true;
438
439                String mn = target.name();
440                // skip checking re-exports for java.base
441                if (origin == target || "java.base".equals(mn))
442                    return false;
443
444                assert origin.requires().containsKey(mn);  // otherwise, should not be accessible
445                if (origin.requires().get(mn)) {
446                    return false;
447                }
448                return true;
449            }
450        };
451    }
452}
453