Skip to content

Commit fd47e54

Browse files
More robust troubleshooting (scottrogowski#34)
* more robust troubleshooting and add js fix * more js fixes and coverage back to 100% * slightly better debugging graphviz
1 parent 5ae4c31 commit fd47e54

8 files changed

Lines changed: 104 additions & 18 deletions

File tree

‎MANIFEST.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
include LICENSE, CHANGELOG.md
2+
include code2flow/get_ast.js

‎code2flow/engine.py‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
VERSION = '2.2.0'
1818

19-
VALID_EXTENSIONS = {'png', 'svg', 'dot', 'gv', 'json'}
19+
IMAGE_EXTENSIONS = ('png', 'svg')
20+
TEXT_EXTENSIONS = ('dot', 'gv', 'json')
21+
VALID_EXTENSIONS = IMAGE_EXTENSIONS + TEXT_EXTENSIONS
2022

2123
DESCRIPTION = "Generate flow charts from your source code. " \
2224
"See the README at https://github.com/scottrogowski/code2flow."
@@ -458,8 +460,11 @@ def _generate_graphviz(output_file, extension, final_img_filename):
458460
logging.info("Running graphviz to make the image...")
459461
command = ["dot", "-T" + extension, output_file]
460462
with open(final_img_filename, 'w') as f:
461-
subprocess.run(command, stdout=f, check=True)
462-
logging.info("Graphviz finished in %.2f seconds." % (time.time() - start_time))
463+
try:
464+
subprocess.run(command, stdout=f, check=True)
465+
logging.info("Graphviz finished in %.2f seconds." % (time.time() - start_time))
466+
except subprocess.CalledProcessError:
467+
logging.warning("*** Graphviz returned non-zero exit code! Try running %r for more detail ***", ' '.join(command + ['-v', '-O']))
463468

464469

465470
def _generate_final_img(output_file, extension, final_img_filename, num_edges):
@@ -512,18 +517,18 @@ def code2flow(raw_source_paths, output_file, language=None, hide_legend=True,
512517

513518
output_ext = None
514519
if isinstance(output_file, str):
515-
assert '.' in output_file, "Output filename must end in one of: %r." % VALID_EXTENSIONS
520+
assert '.' in output_file, "Output filename must end in one of: %r." % set(VALID_EXTENSIONS)
516521
output_ext = output_file.rsplit('.', 1)[1] or ''
517-
assert output_ext in VALID_EXTENSIONS, "Output filename must end in one of: %r." % VALID_EXTENSIONS
522+
assert output_ext in VALID_EXTENSIONS, "Output filename must end in one of: %r." % set(VALID_EXTENSIONS)
518523

519524
final_img_filename = None
520-
if output_ext and output_ext in ('png', 'svg'):
525+
if output_ext and output_ext in IMAGE_EXTENSIONS:
521526
if not is_installed('dot') and not is_installed('dot.exe'):
522527
raise AssertionError(
523528
"Can't generate a flowchart image because neither `dot` nor "
524529
"`dot.exe` was found. Either install graphviz (see the README) "
525-
"or set your --output argument to a 'dot' filename like out.dot "
526-
"or out.gv.")
530+
"or, if you just want an intermediate text file, set your --output "
531+
"file to use a supported text extension: %r" % set(TEXT_EXTENSIONS))
527532
final_img_filename = output_file
528533
output_file, extension = output_file.rsplit('.', 1)
529534
output_file += '.gv'

‎code2flow/javascript.py‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ def resolve_owner(callee):
6666
return OWNER_CONST.UNKNOWN_VAR
6767

6868
if callee['object']['type'] == 'NewExpression':
69-
return callee['object']['callee']['name']
69+
if 'name' in callee['object']['callee']:
70+
return callee['object']['callee']['name']
71+
return djoin(callee['object']['callee']['object']['name'],
72+
callee['object']['callee']['property']['name'])
7073

7174
return OWNER_CONST.UNKNOWN_VAR
7275

@@ -209,7 +212,7 @@ def children(tree):
209212
if type(v) == dict and v.get('type'):
210213
ret.append(v)
211214
if type(v) == list:
212-
ret += v
215+
ret += filter(None, v)
213216
return ret
214217

215218

@@ -231,7 +234,8 @@ def get_acorn_version():
231234
Get the version of installed acorn
232235
:rtype: str
233236
"""
234-
return subprocess.check_output(['node', '-p', 'require(\'acorn/package.json\').version'])
237+
outp = subprocess.check_output(['node', '-p', 'require(\'acorn/package.json\').version'])
238+
return outp.decode().strip()
235239

236240

237241
class Javascript(BaseLanguage):
@@ -241,11 +245,12 @@ def assert_dependencies():
241245
assert is_installed('acorn'), "Acorn is required to parse javascript files " \
242246
"but was not found on the path. Install it " \
243247
"from npm and try again."
244-
245-
if not get_acorn_version().startswith(b'8.'):
248+
version = get_acorn_version()
249+
if not version.startswith('8.'):
246250
logging.warning("Acorn is required to parse javascript files. "
247251
"Version %r was found but code2flow has only been "
248-
"tested on 8.*", get_acorn_version())
252+
"tested on 8.*", version)
253+
logging.info("Using Acorn %s" % version)
249254

250255
@staticmethod
251256
def get_tree(filename, lang_params):
@@ -266,7 +271,8 @@ def get_tree(filename, lang_params):
266271
"Acorn could not parse file %r. You may have a JS syntax error or "
267272
"if this is an es6-style source, you may need to run code2flow "
268273
"with --source-type=module. "
269-
"For more detail, try running the command `acorn %s`. "
274+
"For more detail, try running the command "
275+
"\n acorn %s\n"
270276
"Warning: Acorn CANNOT parse all javascript files. See their docs. " %
271277
(filename, filename)) from None
272278
tree = json.loads(output)
@@ -331,7 +337,12 @@ def make_nodes(tree, parent):
331337
full_node_body = tree['value']
332338

333339
subgroup_trees, subnode_trees, this_scope_body = Javascript.separate_namespaces(full_node_body)
334-
assert not subgroup_trees
340+
if subgroup_trees:
341+
# TODO - this is when a class is defined within a function
342+
# It's unusual but should probably be handled in the future.
343+
# Handling this use case would require some code reorganziation.
344+
# Take a look at class_in_function.js to better understand.
345+
logging.warning("Skipping class defined within a function!")
335346

336347
line_number = lineno(tree)
337348
calls = make_calls(this_scope_body)

‎setup.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
download_url=download_url,
2222
packages=['code2flow'],
2323
python_requires='>=3.6',
24+
include_package_data=True,
2425
classifiers=[
2526
'Natural Language :: English',
2627
"Programming Language :: Python :: 3",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function rectangleClassFactory() {
2+
class Rectangle {
3+
constructor(height, width) {
4+
this.height = height;
5+
this.width = width;
6+
}
7+
}
8+
return Rectangle
9+
}
10+
11+
rectangleClassFactory()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class Abra {
2+
init() {
3+
return this.init()
4+
}
5+
}
6+
7+
class Cadabra {
8+
init() {
9+
return this.init()
10+
}
11+
}
12+
13+
function abra_fact() {
14+
return Abra;
15+
}
16+
17+
function cadabra_fact() {
18+
return Cadabra;
19+
}
20+
21+
22+
class ClassMap {
23+
fact(which) {
24+
return which == "abra" ? abra_fact : cadabra_fact;
25+
}
26+
}
27+
28+
obj = true ? new ClassMap.fact("abra").init(n) : new ClassMap.fact("cadabra").init(n)
29+
obj = true ? new ClassMap.fact("abra").fact().init(n) : new ClassMap.fact("cadabra").fact().init(n)

‎tests/test_interface.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
sys.path.append(os.getcwd().split('/tests')[0])
1111

12-
from code2flow.engine import code2flow, main
12+
from code2flow.engine import code2flow, main, _generate_graphviz
1313
from code2flow import model
1414

1515
IMG_PATH = '/tmp/code2flow/output.png'
@@ -57,6 +57,13 @@ def test_no_files():
5757
output_file=IMG_PATH)
5858

5959

60+
def test_graphviz_error(caplog):
61+
caplog.set_level(logging.DEBUG)
62+
_generate_graphviz("/tmp/code2flow/nothing", "/tmp/code2flow/nothing",
63+
"/tmp/code2flow/nothing")
64+
assert "non-zero exit" in caplog.text
65+
66+
6067
def test_no_files_2():
6168
if not os.path.exists('/tmp/code2flow/no_source_dir'):
6269
os.mkdir('/tmp/code2flow/no_source_dir')
@@ -130,7 +137,7 @@ def test_repr():
130137

131138
def test_bad_acorn(mocker, caplog):
132139
caplog.set_level(logging.DEBUG)
133-
mocker.patch('code2flow.javascript.get_acorn_version', return_value=b'7.6.9')
140+
mocker.patch('code2flow.javascript.get_acorn_version', return_value='7.6.9')
134141
code2flow("test_code/js/simple_a_js", "/tmp/code2flow/out.json")
135142
assert "Acorn" in caplog.text and "8.*" in caplog.text
136143

‎tests/testdata.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,27 @@
598598
"expected_edges": [["file_a_good::(global)", "file_a_good::a"]],
599599
"expected_nodes": ["file_a_good::(global)", "file_a_good::a"]
600600
},
601+
{
602+
"test_name": "class_in_function",
603+
"directory": "class_in_function",
604+
"comments": "when a function defines a class within it",
605+
"expected_edges": [["class_in_function::(global)",
606+
"class_in_function::rectangleClassFactory"]],
607+
"expected_nodes": ["class_in_function::rectangleClassFactory",
608+
"class_in_function::(global)"]
609+
},
610+
{
611+
"test_name": "ternary_new",
612+
"directory": "ternary_new",
613+
"comments": "Ignore the name. This is for complex multi-layered object instantiation.",
614+
"expected_edges": [["ternary_new::Cadabra.init", "ternary_new::Cadabra.init"],
615+
["ternary_new::Abra.init", "ternary_new::Abra.init"],
616+
["ternary_new::(global)", "ternary_new::ClassMap.fact"]],
617+
"expected_nodes": ["ternary_new::Cadabra.init",
618+
"ternary_new::ClassMap.fact",
619+
"ternary_new::(global)",
620+
"ternary_new::Abra.init"]
621+
},
601622
{
602623
"test_name": "moment.js",
603624
"directory": "moment",

0 commit comments

Comments
 (0)