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