Skip to content

Commit

Permalink
framework feature location
Browse files Browse the repository at this point in the history
  • Loading branch information
amiraliakbari committed Aug 21, 2013
1 parent 156d049 commit f81fe14
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 13 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,3 @@ nosetests.xml

# IDEs
/.idea/*

# Project Specific
/samples/code_visualization/reports/*
48 changes: 45 additions & 3 deletions inspector/analyzer/feature/framework_feature_analyzer.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
# -*- coding: utf-8 -*-
import re
from networkx import Graph


class FrameworkFeatureAnalyzer(object):
def __init__(self, framework, project):
"""
:param inspector.models.base.Project project: the project to be analyzed
"""
self.framework_namespace = framework
self.project = project

def analyze_file_imports(self, source_file):
self.framework_namespace = str(framework)
self.framework_tree = Graph()
self.framework_tree.add_node(self.framework_namespace)
self.import_usages = []

def analyze_framework_imports(self, source_file):
"""
:param inspector.models.base.SourceFile source_file: the file
"""
for im in source_file.imports:
if im.import_str.startswith(self.framework_namespace):
print im
self.import_usages.append((im, im.find_usages()))

components = im.import_str.split('.')

data = {'group': 1}
if re.match(r'^[A-Z]+(_[A-Z]+)*$', components[-1]):
data['group'] = 3

last = None
for i in range(len(components)):
cn = '.'.join(components[:i + 1])
self.framework_tree.add_node(cn, **data)
if last:
self.framework_tree.add_edge(last, cn, weight=1, group=1)
last = cn
if last:
data['group'] = 3
self.framework_tree.add_node(last, **data)

def analyze_source(self, source_file):
"""
:param inspector.models.base.SourceFile source_file: the file
"""
for cl in source_file.classes:
self.framework_tree.add_node(cl.name, group=4)
for fu in cl.methods:
# print '[{0}-{1}]'.format(fu.starting_line, fu.ending_line), re.sub('\s*\n\s*', ' ', unicode(fu))
fn = fu.qualified_name
self.framework_tree.add_node(fn, group=5)
self.framework_tree.add_edge(cl.name, fn, weight=1, group=2)
for im, usages in self.import_usages:
w = 0
for ln in usages:
if fu.starting_line <= ln <= fu.ending_line:
w += 1
if w:
self.framework_tree.add_edge(im.import_str, fn, weight=w, group=3)
8 changes: 8 additions & 0 deletions inspector/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ def build_source_file(filename):
class Import(object):
def __init__(self, import_str, source_file=None):
"""
:param str or unicode import_str: the string used to import the identifier
:param SourceFile source_file: the source file this import is located in
"""
self.import_str = import_str
Expand Down Expand Up @@ -606,6 +607,13 @@ def set_parent_class(self, parent_class):
def is_constructor(self):
return self.return_type is None

@property
def qualified_name(self):
name = self.name
if self.parent_class:
name = self.parent_class.name + '.' + name # TODO: this must be self.parent_class.qualified_name
return name

@classmethod
def parse_access(cls, access_str, default=None):
if access_str is not None:
Expand Down
3 changes: 2 additions & 1 deletion inspector/models/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ def try_parse(cls, string, opts=None):
class JavaMethod(Method, LanguageSpecificParser):
# TODO: initial groups may come in other orders
# TODO: better detection of templates
METHOD_RE = re.compile(r'^(@[a-zA-Z0-9_]+\s+)?([a-z]+\s+)?(static\s+)?(synchronized\s+)?([a-zA-Z0-9._<>]+\s+)?(\w+)\s*\((.*)\)(?:\s*throws ([a-zA-Z0-9<>_.,]+))?$')
METHOD_RE = re.compile(r'^(@[a-zA-Z0-9_]+\s+)?([a-z]+\s+)?(static\s+)?(synchronized\s+)?([a-zA-Z0-9._<>]+\s+)?(\w+)\s*\((.*?)\)(?:\s*throws ([a-zA-Z0-9<>_.,]+))?$',
re.DOTALL)

def __init__(self, *args, **kwargs):
self.synchronized = kwargs.pop(u'synchronized', None) or False
Expand Down
18 changes: 17 additions & 1 deletion inspector/test/utils_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
# -*- coding: utf-8 -*-
import unittest
from inspector.utils.strings import has_word, quoted
from inspector.utils.strings import has_word, quoted, summarize


class StringsTest(unittest.TestCase):
def test_summarize(self):
sample_str = 'Short String'
self.assertEqual(summarize(sample_str, max_len=20), sample_str)
self.assertEqual(summarize(sample_str, max_len=len(sample_str)), sample_str)
self.assertEqual(summarize(sample_str, max_len=5), 'Short...')

# Word breaking
self.assertEqual(summarize(sample_str, max_len=6), 'Short ...')
self.assertEqual(summarize(sample_str, max_len=8), 'Short St...')
# TODO: this is the correct behavior:
#self.assertEqual(summarize(sample_str, max_len=6), 'Short...')
#self.assertEqual(summarize(sample_str, max_len=8), 'Short ...')

# unicode
self.assertEqual(summarize(u'رشته‌ی مثال', max_len=6), u'رشته‌ی...')

def test_quoted(self):
self.assertEqual(quoted('string'), '"string"')
self.assertEqual(quoted('first second third'), '"first second third"')
Expand Down
12 changes: 12 additions & 0 deletions inspector/utils/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ def has_word(string, sub):
""" Determine if the string has the given sub string as a whole word in it
"""
return re.search(r"\b" + re.escape(sub) + r"\b", string) is not None


def render_template(template_string, params):
"""
:param str template_string: string to render
:param dict params: a dictionary containing template parameters
:rtype: str
"""

for k, v in params.iteritems():
template_string = template_string.replace('{{ ' + k + ' }}', unicode(v))
return template_string
1 change: 1 addition & 0 deletions inspector/utils/visualization/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
36 changes: 36 additions & 0 deletions inspector/utils/visualization/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import os

from inspector.utils.strings import render_template


def generate_graph_html(graph, filename):
"""
:type graph: networkx.Graph
:param str filename: path to save the generated html file
"""

params = {
'nodes': [],
'links': [],
}
ids = {}
for i, (node, data) in enumerate(graph.nodes(data=True)):
val = unicode(node)
ids[val] = i
params['nodes'].append('{name:"%s",group:%d}' % (node, data.get('group', 1)))
for u, v, data in graph.edges(data=True):
params['links'].append('{source:%d,target:%d,value:%d,group:%d}' % (ids[unicode(u)],
ids[unicode(v)],
data.get('weight', 1),
data.get('group', 1)))
params['nodes'] = ','.join(params['nodes'])
params['links'] = ','.join(params['links'])

# generating output
current_dir = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(current_dir, 'templates', 'force_directed_graph.html'), 'r') as f:
html = f.read()
html = render_template(html, params)
with open(filename, 'w') as f:
f.write(html)
73 changes: 73 additions & 0 deletions inspector/utils/visualization/templates/force_directed_graph.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}

.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var graph = {
nodes: [{{ nodes }}],
links: [{{ links }}]
};
</script>
<script>
var width = 960,
height = 500;

var color = d3.scale.category20();

var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);

force
.nodes(graph.nodes)
.links(graph.links)
.start();

var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });

var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);

node.append("title")
.text(function(d) { return d.name; });

force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });

node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
</script>
</body>
</html>
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
networkx
4 changes: 2 additions & 2 deletions samples/code_visualization/visualize_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from inspector.analyzer.project_analyzer import LocCounter, LocCounter2
from inspector.models.base import Project
from inspector.utils.strings import render_template


if __name__ == '__main__':
Expand Down Expand Up @@ -53,7 +54,6 @@

with open(os.path.join(current_dir, 'report_templates', html_template), 'r') as f:
html = f.read()
for k, v in params.iteritems():
html = html.replace('{{ ' + k + ' }}', unicode(v))
html = render_template(html, params)
with open(os.path.join(current_dir, 'reports', 'report.html'), 'w') as f:
f.write(html)
1 change: 0 additions & 1 deletion samples/feature_location/fl.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
project_type = str(raw_input('What is the project type? [java/android/auto] '))
analysis_type = str(raw_input('Select the analysis to perform on this project: [dynamic/framework-features] '))
else:
print sys.argv
project_dir = sys.argv[1]
project_type = sys.argv[2]
analysis_type = sys.argv[3]
Expand Down
12 changes: 10 additions & 2 deletions samples/feature_location/framework_fl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
import os

from inspector.analyzer.feature.framework_feature_analyzer import FrameworkFeatureAnalyzer
from inspector.utils.visualization.graph import generate_graph_html


def framework_features_analyze(project):
Expand All @@ -11,5 +14,10 @@ def framework_features_analyze(project):
print '===================================='

fa = FrameworkFeatureAnalyzer('android', project=project)
# TODO: use package format
fa.analyze_file_imports(project.get_file('app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java'))
# TODO: use package format:
source_file = project.get_file('app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java')
fa.analyze_framework_imports(source_file)
fa.analyze_source(source_file)

current_dir = os.path.abspath(os.path.dirname(__file__))
generate_graph_html(fa.framework_tree, os.path.join(current_dir, 'reports', 'report.html'))

0 comments on commit f81fe14

Please sign in to comment.