/** * Copyright 2009, Google Inc. All rights reserved. * Licensed to PSF under a Contributor Agreement. */ package org.python.indexer; import org.antlr.runtime.ANTLRFileStream; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.RecognitionException; import org.python.antlr.AnalyzingParser; import org.python.antlr.base.mod; import org.python.indexer.ast.NModule; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * Provides a factory for python source ASTs. Maintains configurable on-disk and * in-memory caches to avoid re-parsing files during analysis. */ public class AstCache { public static final String CACHE_DIR = Util.getSystemTempDir() + "jython/ast_cache/"; private static final Logger LOG = Logger.getLogger(AstCache.class.getCanonicalName()); private Map<String, NModule> cache = new HashMap<String, NModule>(); private static AstCache INSTANCE; private AstCache() throws Exception { File f = new File(CACHE_DIR); if (!f.exists()) { f.mkdirs(); } } public static AstCache get() throws Exception { if (INSTANCE == null) { INSTANCE = new AstCache(); } return INSTANCE; } /** * Clears the memory cache. */ public void clear() { cache.clear(); } /** * Removes all serialized ASTs from the on-disk cache. * @return {@code true} if all cached AST files were removed */ public boolean clearDiskCache() { try { File dir = new File(CACHE_DIR); for (File f : dir.listFiles()) { if (f.isFile()) { f.delete(); } } return true; } catch (Exception x) { severe("Failed to clear disk cache: " + x); return false; } } /** * Returns the syntax tree for {@code path}. May find and/or create a * cached copy in the mem cache or the disk cache. * @param path absolute path to a source file * @return the AST, or {@code null} if the parse failed for any reason * @throws Exception if anything unexpected occurs */ public NModule getAST(String path) throws Exception { if (path == null) throw new IllegalArgumentException("null path"); return fetch(path); } /** * Returns the syntax tree for {@code path} with {@code contents}. * Uses the memory cache but not the disk cache. * This method exists primarily for unit testing. * @param path a name for the file. Can be relative. * @param contents the source to parse */ public NModule getAST(String path, String contents) throws Exception { if (path == null) throw new IllegalArgumentException("null path"); if (contents == null) throw new IllegalArgumentException("null contents"); // Cache stores null value if the parse failed. if (cache.containsKey(path)) { return cache.get(path); } NModule mod = null; try { mod = parse(path, contents); if (mod != null) { mod.setFileAndMD5(path, Util.getMD5(contents.getBytes("UTF-8"))); } } finally { cache.put(path, mod); // may be null } return mod; } /** * Get or create an AST for {@code path}, checking and if necessary updating * the disk and memory caches. * @param path absolute source path */ private NModule fetch(String path) throws Exception { // Cache stores null value if the parse failed. if (cache.containsKey(path)) { return cache.get(path); } // Might be cached on disk but not in memory. NModule mod = getSerializedModule(path); if (mod != null) { fine("reusing " + path); cache.put(path, mod); return mod; } try { mod = parse(path); } finally { cache.put(path, mod); // may be null } if (mod != null) { serialize(mod); } return mod; } /** * Parse a file. Does not look in the cache or cache the result. */ private NModule parse(String path) throws Exception { fine("parsing " + path); mod ast = invokeANTLR(path); return generateAST(ast, path); } /** * Parse a string. Does not look in the cache or cache the result. */ private NModule parse(String path, String contents) throws Exception { fine("parsing " + path); mod ast = invokeANTLR(path, contents); return generateAST(ast, path); } private NModule generateAST(mod ast, String path) throws Exception { if (ast == null) { Indexer.idx.reportFailedAssertion("ANTLR returned NULL for " + path); return null; } // Convert to indexer's AST. Type conversion warnings are harmless here. @SuppressWarnings("unchecked") Object obj = ast.accept(new AstConverter()); if (!(obj instanceof NModule)) { warn("\n[warning] converted AST is not a module: " + obj); return null; } NModule module = (NModule)obj; if (new File(path).canRead()) { module.setFile(path); } return module; } private mod invokeANTLR(String filename) { CharStream text = null; try { text = new ANTLRFileStream(filename); } catch (IOException iox) { fine(filename + ": " + iox); return null; } return invokeANTLR(text, filename); } private mod invokeANTLR(String filename, String contents) { CharStream text = new ANTLRStringStream(contents); return invokeANTLR(text, filename); } private mod invokeANTLR(CharStream text, String filename) { AnalyzingParser p = new AnalyzingParser(text, filename, null); mod ast = null; try { ast = p.parseModule(); } catch (Exception x) { fine("parse for " + filename + " failed: " + x); } recordParseErrors(filename, p.getRecognitionErrors()); return ast; } private void recordParseErrors(String path, List<RecognitionException> errs) { if (errs.isEmpty()) { return; } List<Diagnostic> diags = Indexer.idx.getParseErrs(path); for (RecognitionException rx : errs) { String msg = rx.line + ":" + rx.charPositionInLine + ":" + rx; diags.add(new Diagnostic(path, Diagnostic.Type.ERROR, -1, -1, msg)); } } /** * Each source file's AST is saved in an object file named for the MD5 * checksum of the source file. All that is needed is the MD5, but the * file's base name is included for ease of debugging. */ public String getCachePath(File sourcePath) throws Exception { return getCachePath(Util.getMD5(sourcePath), sourcePath.getName()); } public String getCachePath(String md5, String name) { return CACHE_DIR + name + md5 + ".ast"; } // package-private for testing void serialize(NModule ast) throws Exception { String path = getCachePath(ast.getMD5(), new File(ast.getFile()).getName()); ObjectOutputStream oos = null; FileOutputStream fos = null; try { fos = new FileOutputStream(path); oos = new ObjectOutputStream(fos); oos.writeObject(ast); } finally { if (oos != null) { oos.close(); } else if (fos != null) { fos.close(); } } } // package-private for testing NModule getSerializedModule(String sourcePath) { try { File sourceFile = new File(sourcePath); if (sourceFile == null || !sourceFile.canRead()) { return null; } File cached = new File(getCachePath(sourceFile)); if (!cached.canRead()) { return null; } return deserialize(sourceFile); } catch (Exception x) { severe("Failed to deserialize " + sourcePath + ": " + x); return null; } } // package-private for testing NModule deserialize(File sourcePath) throws Exception { String cachePath = getCachePath(sourcePath); FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(cachePath); ois = new ObjectInputStream(fis); NModule mod = (NModule)ois.readObject(); // Files in different dirs may have the same base name and contents. mod.setFile(sourcePath); return mod; } finally { if (ois != null) { ois.close(); } else if (fis != null) { fis.close(); } } } private void log(Level level, String msg) { if (LOG.isLoggable(level)) { LOG.log(level, msg); } } private void severe(String msg) { log(Level.SEVERE, msg); } private void warn(String msg) { log(Level.WARNING, msg); } private void info(String msg) { log(Level.INFO, msg); } private void fine(String msg) { log(Level.FINE, msg); } private void finer(String msg) { log(Level.FINER, msg); } }