All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.psr.vulas.java.JavaFileAnalyzer2 Maven / Gradle / Ivy

There is a newer version: 3.1.15
Show newest version
/**
 * This file is part of Eclipse Steady.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved.
 */
package com.sap.psr.vulas.java;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sap.psr.vulas.Construct;
import com.sap.psr.vulas.ConstructId;
import com.sap.psr.vulas.FileAnalysisException;
import com.sap.psr.vulas.FileAnalyzer;
import com.sap.psr.vulas.java.antlr.JavaLexer;
import com.sap.psr.vulas.java.antlr.JavaParser;
import com.sap.psr.vulas.java.antlr.JavaParser.CompilationUnitContext;
import com.sap.psr.vulas.java.antlr.JavaParser.FormalParameterContext;
import com.sap.psr.vulas.java.antlr.JavaParser.TypeTypeContext;
import com.sap.psr.vulas.java.antlr.JavaParserBaseListener;
import com.sap.psr.vulas.shared.util.FileUtil;

/**
 * Analyzes java source files using ANTLR.
 */
public class JavaFileAnalyzer2 extends JavaParserBaseListener implements FileAnalyzer {

	private static final Log log = LogFactory.getLog(JavaFileAnalyzer2.class);

	/**
	 * All Java constructs found in the given Java file, created through visiting relevant nodes of the ANTLR parse tree.
	 * The values are return by {@link #getConstructs()}. Java enums and interfaces are ignored, as they rarely contain
	 * executable code.
	 */
	private Map constructs = null;

	private ANTLRInputStream input = null;

	/** The file to be analyzed. */
	private File file = null;

	/**
	 * Package, class, enum and interface declarations found while parsing a Java source file.
	 * The topmost element will be used as declaration context of methods and constructors.
	 */
	private final ContextStack contextStack = new ContextStack();

	/** Used for the construction of nested named and anonynous classes. */
	private final ConstructIdBuilder constructIdBuilder = new ConstructIdBuilder();
	
	/** {@inheritDoc} */
	@Override
	public String[] getSupportedFileExtensions() {
		return new String[] { "java" };
	}
	
	/** {@inheritDoc} */
	@Override
	public boolean canAnalyze(File _file) {
		final String ext = FileUtil.getFileExtension(_file);
		if(ext == null || ext.equals(""))
			return false;
		for(String supported_ext: this.getSupportedFileExtensions()) {
			if(supported_ext.equalsIgnoreCase(ext))
				return true;
		}
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public void analyze(final File _file) throws FileAnalysisException {
		this.setFile(_file);
	}
	
	/**
	 * 

Setter for the field file.

* * @param _file a {@link java.io.File} object. * @throws java.lang.IllegalArgumentException if any. */ public void setFile(File _file) throws IllegalArgumentException { final String ext = FileUtil.getFileExtension(_file); if(!ext.equals("java")) throw new IllegalArgumentException("Expected a java file but got [" + _file + "]"); if(!FileUtil.isAccessibleFile(_file.toPath())) throw new IllegalArgumentException("Cannot open file [" + _file + "]"); this.file = _file; } /** * Creates and adds a new {@link Construct} to the set of constructs found in the analyzed file. * This method is called during the various visitor methods inherited from {@link JavaBaseListener}. * @param _id * @param _body */ private void saveConstruct(ConstructId _id, String _body) { try { final Construct c = new Construct(_id, _body); this.constructs.put(_id, c); JavaFileAnalyzer2.log.debug("Added " + c.getId()); } catch (IllegalArgumentException e) { JavaFileAnalyzer2.log.error(e); } } /** {@inheritDoc} */ @Override public void enterPackageDeclaration(@NotNull JavaParser.PackageDeclarationContext ctx) { // Create JavaId final JavaPackageId id = new JavaPackageId(ctx.getChild(1).getText()); // Add to the stack this.contextStack.push(id); // Create the construct this.saveConstruct(id, ctx.getParent().getText()); } /** * {@inheritDoc} * * Enums are added to {@link #constructs}. */ @Override public void enterEnumDeclaration(@NotNull JavaParser.EnumDeclarationContext ctx) { // Create JavaId and push to the stack final ContextStackEntry cse = this.contextStack.peek(); final JavaId decl_ctx = ( cse==null ? JavaPackageId.DEFAULT_PACKAGE : (JavaId)cse.getConstructId() ); final JavaId id = new JavaEnumId(decl_ctx, ctx.IDENTIFIER().getText()); this.contextStack.push(id); this.saveConstruct(id, this.getConstructContent(ctx)); } /** {@inheritDoc} */ @Override public void exitEnumDeclaration(@NotNull JavaParser.EnumDeclarationContext ctx) { final JavaId id = (JavaId)this.contextStack.pop().getConstructId(); this.isOfExpectedType(id, new JavaId.Type[] { JavaId.Type.ENUM }, true); } /** * {@inheritDoc} * * Interfaces are not added to {@link #constructs}. */ @Override public void enterInterfaceDeclaration(@NotNull JavaParser.InterfaceDeclarationContext ctx) { // Create JavaId and push to the stack final ContextStackEntry cse = this.contextStack.peek(); final JavaId decl_ctx = ( cse==null ? JavaPackageId.DEFAULT_PACKAGE : (JavaId)cse.getConstructId() ); final JavaId id = new JavaInterfaceId(decl_ctx, ctx.IDENTIFIER().getText()); this.contextStack.push(id); } /** {@inheritDoc} */ @Override public void exitInterfaceDeclaration(@NotNull JavaParser.InterfaceDeclarationContext ctx) { final JavaId id = (JavaId)this.contextStack.pop().getConstructId(); this.isOfExpectedType(id, new JavaId.Type[] { JavaId.Type.INTERFACE }, true); } /** {@inheritDoc} */ @Override public void enterClassDeclaration(@NotNull JavaParser.ClassDeclarationContext ctx) { // Remember the declaration context (needed for the handling of anon. classes in enterClassBody) this.constructIdBuilder.setCurrentDeclarationContext(ctx.IDENTIFIER().getText()); } /** {@inheritDoc} */ @Override public void exitClassDeclaration(@NotNull JavaParser.ClassDeclarationContext ctx) {} /** {@inheritDoc} */ @Override public void enterClassBody(@NotNull JavaParser.ClassBodyContext ctx) { // Create JavaId and push to the stack final JavaId id = (JavaClassId) this.constructIdBuilder.buildJavaClassId(); this.contextStack.push(id); this.saveConstruct(id, this.getConstructContent(ctx)); // Log anon classes if(this.constructIdBuilder.isAnonymousClass()) JavaFileAnalyzer2.log.debug(this.indent(this.contextStack.size()) + "Enter anon class body " + id.toString() + " " + this.printDeclarationStack()); this.constructIdBuilder.resetCurrentDeclarationContext(); } /** {@inheritDoc} */ @Override public void exitClassBody(@NotNull JavaParser.ClassBodyContext ctx) { final JavaId id = (JavaId) this.contextStack.pop().getConstructId(); this.isOfExpectedType(id, new JavaId.Type[] { JavaId.Type.CLASS }, true); this.constructIdBuilder.resetCurrentDeclarationContext(); } /** {@inheritDoc} */ @Override public void enterMethodDeclaration(@NotNull JavaParser.MethodDeclarationContext ctx) { // Peek JavaId and ensure it is a class or enum final JavaId class_ctx = (JavaId) this.contextStack.peek().getConstructId(); this.isOfExpectedType(class_ctx, new JavaId.Type[] { JavaId.Type.CLASS, JavaId.Type.ENUM }, true); // Build the identifier final JavaMethodId id = new JavaMethodId((JavaId) class_ctx, ctx.IDENTIFIER().getText(), this.getParameters(ctx.formalParameters().formalParameterList())); this.contextStack.push(id); this.saveConstruct(id, this.getConstructContent(ctx)); } /** {@inheritDoc} */ @Override public void exitMethodDeclaration(com.sap.psr.vulas.java.antlr.JavaParser.MethodDeclarationContext ctx) { final JavaId id = (JavaId) this.contextStack.pop().getConstructId(); } /** {@inheritDoc} */ @Override public void enterConstructorDeclaration(@NotNull JavaParser.ConstructorDeclarationContext ctx) { // Peek JavaId and ensure it is a class or enum final JavaId class_ctx = (JavaId) this.contextStack.peek().getConstructId(); this.isOfExpectedType(class_ctx, new JavaId.Type[] { JavaId.Type.CLASS, JavaId.Type.ENUM }, true); // Build the identifier final JavaId id = new JavaConstructorId((JavaId)class_ctx, this.getParameters(ctx.formalParameters().formalParameterList())); this.contextStack.push(id); this.saveConstruct(id, this.getConstructContent(ctx)); } /** {@inheritDoc} */ @Override public void exitConstructorDeclaration(com.sap.psr.vulas.java.antlr.JavaParser.ConstructorDeclarationContext ctx) { final JavaId id = (JavaId)this.contextStack.pop().getConstructId(); } /** * Retrieves content for constructs of type Method, Constructor and Class. * @param ctx - ParseRuleContex * @return Extracted construct Body */ private final String getConstructContent(ParserRuleContext ctx){ final int a = ctx.start.getStartIndex(); final int b = ctx.stop.getStopIndex(); final Interval interval = new Interval(a,b); final String text = this.input.getText(interval); return text; } private boolean isOfExpectedType(JavaId _jid, JavaId.Type[] _types, boolean _throw_exception) { boolean is = true; if(_jid==null || !Arrays.asList(_types).contains(_jid.getType())) { is = false; if(_throw_exception) { JavaFileAnalyzer2.log.error("Expected [" + _types[0] + "], got " + _jid); throw new IllegalStateException("Expected [" + _types[0] + "], got " + _jid); } else { JavaFileAnalyzer2.log.warn("Expected [" + _types[0] + "], got " + _jid); } is = false; } return is; } /** * Returns true if the construct stack only consists of classes. It allows skipping * all declarations happening in enums and interfaces, nested or not. */ /*private boolean isClassDeclarationsOnly(ParserRuleContext _context) { boolean is = true; for(ContextStackEntry jid : this.contextStack.all()) { if ( !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.PACKAGE) && !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.CLASS) && !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.METHOD) ) { // Get the name of the current declaration String item = null; if (_context instanceof JavaParser.ClassDeclarationContext) item = "class [" + ((JavaParser.ClassDeclarationContext) _context).Identifier().getText() + "]"; // else if (_context instanceof // JavaParser.MethodDeclarationContext) // item = "method [" + ((JavaParser.MethodDeclarationContext) // _context).Identifier().getText() + "]"; else if (_context instanceof JavaParser.ConstructorDeclarationContext) item = "constructor [" + ((JavaParser.ConstructorDeclarationContext) _context).Identifier().getText() + "]"; else if (_context instanceof JavaParser.ClassBodyContext) item = "classBody"; // if(_context instanceof JavaParser.ClassDeclarationContext) // item = "class [" + ((JavaParser.ClassDeclarationContext)_context).Identifier().getText() + "]"; // else if(_context instanceof JavaParser.MethodDeclarationContext) // item = "method [" + ((JavaParser.MethodDeclarationContext)_context).Identifier().getText() + "]"; // else if(_context instanceof JavaParser.ConstructorDeclarationContext) // item = "constructor [" + ((JavaParser.ConstructorDeclarationContext)_context).Identifier().getText() + "]"; JavaFileAnalyzer2.log.info("Declaration of " + item + " will be skipped, it is inside a nested declarations including enums and/or interfaces"); is = false; break; } } return is; }*/ /*private boolean isClassDeclarationsOnly(ParserRuleContext _context) { boolean is = true; for(ContextStackEntry jid : this.contextStack.all()) { if ( !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.PACKAGE) && !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.CLASS) && !((JavaId)jid.getConstructId()).getType().equals(JavaId.Type.METHOD) ) { // Get the name of the current declaration String item = null; if (_context instanceof JavaParser.ClassDeclarationContext) item = "class [" + ((JavaParser.ClassDeclarationContext) _context).Identifier().getText() + "]"; // else if (_context instanceof // JavaParser.MethodDeclarationContext) // item = "method [" + ((JavaParser.MethodDeclarationContext) // _context).Identifier().getText() + "]"; else if (_context instanceof JavaParser.ConstructorDeclarationContext) item = "constructor [" + ((JavaParser.ConstructorDeclarationContext) _context).Identifier().getText() + "]"; else if (_context instanceof JavaParser.ClassBodyContext) item = "classBody"; // if(_context instanceof JavaParser.ClassDeclarationContext) // item = "class [" + ((JavaParser.ClassDeclarationContext)_context).Identifier().getText() + "]"; // else if(_context instanceof JavaParser.MethodDeclarationContext) // item = "method [" + ((JavaParser.MethodDeclarationContext)_context).Identifier().getText() + "]"; // else if(_context instanceof JavaParser.ConstructorDeclarationContext) // item = "constructor [" + ((JavaParser.ConstructorDeclarationContext)_context).Identifier().getText() + "]"; JavaFileAnalyzer2.log.info("Declaration of " + item + " will be skipped, it is inside a nested declarations including enums and/or interfaces"); is = false; break; } } return is; }*/ private List getParameters(JavaParser.FormalParameterListContext _ctx) { if(_ctx==null) return null; else { List l = new ArrayList(); List list = _ctx.formalParameter(); for(FormalParameterContext par_ctx: list) { TypeTypeContext type_ctx = par_ctx.typeType(); String t = type_ctx.getText(); // Simply remove the parameterization of generic classes // This is possible, as they do not matter in terms of method overloading // Example: to methods foo(Set) and foo(Set) are not possible within one class if((t.contains("<") || t.contains(">")) && t.indexOf("<") !=-1){ t = t.substring(0, t.indexOf("<")); } // Parameters of simple types (boolean, int, etc.) can be added as is if(type_ctx.primitiveType()!=null) l.add(t); //l.add(type_ctx.primitiveType().getText()); // Parameters of complex types (class or interface) may or may not be specified using its qualified name (i.e., with its package). Since we cannot (easily) add the package information // for those where the package is missing, we simply remove it for all of them (and do the same in the instrumentation). else if(type_ctx.classOrInterfaceType()!=null) l.add(JavaId.removePackageContext(t)); //l.add(JavaId.removePackageContext(type_ctx.classOrInterfaceType().getText())); else JavaFileAnalyzer2.log.error("Parameter " + par_ctx.variableDeclaratorId().getText() + " has unknown type"); } return l; } } /** {@inheritDoc} */ @Override public Map getConstructs() throws FileAnalysisException { if(this.constructs==null) { try { this.constructs = new TreeMap(); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (final InputStream is2 = new BufferedInputStream(new FileInputStream(this.file))) { int cc = -1; while ((cc = is2.read()) >= 0) baos.write(cc); } baos.flush(); this.input = new ANTLRInputStream(new ByteArrayInputStream(baos.toByteArray())); JavaLexer lexer = new JavaLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); JavaParser parser = new JavaParser(tokens); CompilationUnitContext ctx = parser.compilationUnit(); ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(this, ctx); } catch (FileNotFoundException e) { throw new FileAnalysisException(e.getMessage(), e); } catch (RecognitionException e) { throw new FileAnalysisException("ANTLR exception while analysing class file [" + this.file.getName() + "]: " + e.getMessage(), e); } catch (IOException e) { throw new FileAnalysisException("I/O exception while analysing class file [" + this.file.getName() + "]: " + e.getMessage(), e); } catch (Exception e) { throw new FileAnalysisException("Exception of type [" + e.getClass().getSimpleName() + "] while analyzing file [" + this.file.toPath().toAbsolutePath() + "]: " + e.getMessage(), e); } } return this.constructs; } /** {@inheritDoc} */ @Override public boolean containsConstruct(ConstructId _id) throws FileAnalysisException { return this.getConstructs().containsKey(_id); } /** {@inheritDoc} */ @Override public Construct getConstruct(ConstructId _id) throws FileAnalysisException { return this.getConstructs().get(_id); } /** {@inheritDoc} */ @Override public boolean hasChilds() { return false; } /** {@inheritDoc} */ @Override public Set getChilds(boolean _recursive) { return null; } private final String indent(int _i) { StringBuilder b = new StringBuilder(); for (int i = 0; i < _i; i++) b.append(": "); return b.toString(); } private String printDeclarationStack() { final StringBuilder out = new StringBuilder().append("\t\t\t"); for (ContextStackEntry jid : this.contextStack.all()) { out.append(((JavaId) jid.getConstructId()).getType().toString() + " "); } return out.toString(); } class ConstructIdBuilder { // class name as obtained from the enterClassDeclaration callback // that callback is not called for anonymous classes, for which the // member below stays 'null' // (note: it's reset to null in enterClassBody) private String declaredName = null; /** * Used to give numeric names to anonymous inner classes. */ private Map anonymousClassCounters = new HashMap(); /** * Used to prepend numeric values to named classes declared in methods. */ private Map> namedClassesCounter = new HashMap>(); public void setCurrentDeclarationContext(String name) { this.declaredName = name; } public String getDeclaredName() { return this.declaredName; } public void resetCurrentDeclarationContext() { this.declaredName = null; } /** * Returns true if the class for which the name is about to be build is * anonymous * * @return */ private boolean isAnonymousClass() { return this.declaredName == null; } /** * Returns the current counter value for anonymous classes and increases the counter by one. * @param id * @return */ private Integer incrementAnonymousCounter(ConstructId id) { Integer count = null; // Initialize if not done already if(!this.anonymousClassCounters.containsKey(id)) this.anonymousClassCounters.put(id, 1); // Current value count = this.anonymousClassCounters.get(id); // Increase by one this.anonymousClassCounters.put(id, count+1); return count; } /** * Returns the current counter value for named classes and increases the counter by one. * @param id * @return */ private Integer incrementNamedCounter(ConstructId id, String _class_name) { Integer count = null; Map name_counter = this.namedClassesCounter.get(id); // Initialize if necessary if(name_counter==null) { name_counter = new HashMap(); this.namedClassesCounter.put(id, name_counter); } // if(!name_counter.containsKey(_class_name)) name_counter.put(_class_name, 1); // Current value count = name_counter.get(_class_name); // Increase by one name_counter.put(_class_name, count+1); return count; } /** * Create a name for the construct at hand considering what containers * are currently on the stack * * @param spaceId * of the construct (the suffix to add to the container * construct) * @return */ public ConstructId buildJavaClassId() { final JavaId.Type[] cie = new JavaId.Type[] { JavaId.Type.CLASS, JavaId.Type.INTERFACE, JavaId.Type.ENUM }; final JavaId.Type[] pcie = new JavaId.Type[] { JavaId.Type.PACKAGE, JavaId.Type.CLASS, JavaId.Type.INTERFACE, JavaId.Type.ENUM }; // Class name of the new class final StringBuilder class_name = new StringBuilder(); // The context of the to-be-created class (can be PACK, CLASS, INTERFACE or ENUM), depending on the case JavaId context = null; final ContextStackEntry topmost_stack_entry = contextStack.peek(); // Named class if(!this.isAnonymousClass()) { // In method if (topmost_stack_entry!=null && ( (JavaId)topmost_stack_entry.getConstructId()).type.equals(JavaId.Type.METHOD)) { // Get the context (class, interface or enum) final ContextStackEntry cse = contextStack.peek(cie); if(cse==null) { throw new IllegalStateException("Named class [" + this.declaredName + "] w/o appropriate context"); } else { // Get and increment name counter context = (JavaId)cse.getConstructId(); class_name.append(this.incrementNamedCounter(context, this.declaredName).toString() + this.declaredName); } } // In class else { // Get the context (pack, class, interface or enum) final ContextStackEntry cse = contextStack.peek(pcie); if(cse!=null) context = (JavaId)cse.getConstructId(); // Name is what we found in enterClassDeclaration class_name.append(this.declaredName); } } // Anon class else { // Fetch the parent (class, enum or interface) final ContextStackEntry cse = contextStack.peek(cie); if(cse==null) { throw new IllegalStateException("Anonnymous class [" + this.declaredName + "] w/o appropriate context"); } else { // Get and increment anon counter context = (JavaId)cse.getConstructId(); class_name.append(this.incrementAnonymousCounter(context).toString()); } } final ConstructId id = new JavaClassId(context, class_name.toString()); return id; } } static class ContextStackEntry { private ConstructId constructId = null; private Map attributes = new HashMap(); public ContextStackEntry(ConstructId id) { this.constructId = id; } public ContextStackEntry(ConstructId id, Object key, Object value) { this.constructId = id; this.setAttribute(key, value); } public ConstructId getConstructId() { return this.constructId; } public Object getAttribute(Object key) { return this.attributes.get(key); } public void setAttribute(Object key, Object value) { this.attributes.put(key, value); } } static class ContextStack { /** * Nested package, class, enum and interface declarations found while * parsing a Java source file. The topmost element will be used as * declaration context of methods and constructors. */ private Deque nestedDeclarationContexts = new ArrayDeque(); /** * Return the topmost element (without removing it) * * @return */ public ContextStackEntry peek() { return this.nestedDeclarationContexts.peek(); } /** * Gets the top-most element of a given types (without removing it). * * @param _t * the type of context elements to consider * @return */ public ContextStackEntry peek(JavaId.Type[] _t) { final Iterator iterator = this.nestedDeclarationContexts.iterator(); ContextStackEntry entry = null, result = null; search: while (iterator.hasNext()) { entry = iterator.next(); for(int i=0; i<_t.length; i++ ){ if (((JavaId)entry.getConstructId()).getType().equals(_t[i])) { result = entry; break search; } } } return result; } public void push(ConstructId id) { this.nestedDeclarationContexts.push(new ContextStackEntry(id)); } public void push(ContextStackEntry id) { this.nestedDeclarationContexts.push(id); } public ContextStackEntry pop() { return this.nestedDeclarationContexts.pop(); } // note: returns true if either the stack or the pattern is empty! @Deprecated public boolean headMatches(JavaId.Type[] pattern) { boolean result = true; int patternElementIndex = 0; ContextStackEntry construct = null; Iterator itr = this.iterator(); while (itr.hasNext() && patternElementIndex < pattern.length) { construct = itr.next(); if (!pattern[patternElementIndex].equals(((JavaId) construct.getConstructId()).type)) { result = false; break; } else { patternElementIndex++; } } return result; } public int size() { return this.nestedDeclarationContexts.size(); } public Deque all() { return this.nestedDeclarationContexts; } public Iterator iterator() { return this.nestedDeclarationContexts.iterator(); } } }