diff --git a/exemplos_codigos/imagens/c/2.dot b/exemplos_codigos/imagens/c/2.dot new file mode 100644 index 0000000000000000000000000000000000000000..d53a91ec34a31586c5828d75b54c74cb39a1be40 --- /dev/null +++ b/exemplos_codigos/imagens/c/2.dot @@ -0,0 +1,141 @@ +digraph "../exemplos/2.c.015t.cfg" { +overlap=false; +subgraph "cluster_main" { + style="dashed"; + color="black"; + label="main ()"; + subgraph cluster_0_2 { + style="filled"; + color="darkgreen"; + fillcolor="grey88"; + label="loop 2"; + labeljust=l; + penwidth=2; + fn_0_basic_block_9 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 9\>:\l\ +|_9\ =\ array[middle];\l\ +|search.3_10\ =\ search;\l\ +|if\ (_9\ !=\ search.3_10)\l\ +\ \ goto\ \<bb\ 10\>;\ [INV]\l\ +else\l\ +\ \ goto\ \<bb\ 11\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_10 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 10\>:\l\ +|if\ (first\ \<=\ last)\l\ +\ \ goto\ \<bb\ 6\>;\ [INV]\l\ +else\l\ +\ \ goto\ \<bb\ 11\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_6 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 6\>:\l\ +|_5\ =\ array[middle];\l\ +|search.2_6\ =\ search;\l\ +|if\ (_5\ \<\ search.2_6)\l\ +\ \ goto\ \<bb\ 7\>;\ [INV]\l\ +else\l\ +\ \ goto\ \<bb\ 8\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_7 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 7\>:\l\ +|first\ =\ middle\ +\ 1;\l\ +|_7\ =\ first\ +\ last;\l\ +|middle\ =\ _7\ /\ 2;\l\ +goto\ \<bb\ 9\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_8 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 8\>:\l\ +|last\ =\ middle\ +\ -1;\l\ +|_8\ =\ first\ +\ last;\l\ +|middle\ =\ _8\ /\ 2;\l\ +}"]; + + } + subgraph cluster_0_1 { + style="filled"; + color="darkgreen"; + fillcolor="grey88"; + label="loop 1"; + labeljust=l; + penwidth=2; + fn_0_basic_block_4 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 4\>:\l\ +|n.0_2\ =\ n;\l\ +|if\ (c\ \<\ n.0_2)\l\ +\ \ goto\ \<bb\ 3\>;\ [INV]\l\ +else\l\ +\ \ goto\ \<bb\ 5\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_3 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 3\>:\l\ +|_1\ =\ &array[c];\l\ +|scanf\ (\"%d\",\ _1);\l\ +|c\ =\ c\ +\ 1;\l\ +}"]; + + } + fn_0_basic_block_0 [shape=Mdiamond,style=filled,fillcolor=white,label="ENTRY"]; + + fn_0_basic_block_1 [shape=Mdiamond,style=filled,fillcolor=white,label="EXIT"]; + + fn_0_basic_block_2 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 2\>:\l\ +|scanf\ (\"%d\",\ &n);\l\ +|c\ =\ 0;\l\ +goto\ \<bb\ 4\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_5 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 5\>:\l\ +|scanf\ (\"%d\",\ &search);\l\ +|first\ =\ 0;\l\ +|n.1_3\ =\ n;\l\ +|last\ =\ n.1_3\ +\ -1;\l\ +|_4\ =\ first\ +\ last;\l\ +|middle\ =\ _4\ /\ 2;\l\ +goto\ \<bb\ 9\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_11 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 11\>:\l\ +|_11\ =\ array[middle];\l\ +|search.4_12\ =\ search;\l\ +|if\ (_11\ ==\ search.4_12)\l\ +\ \ goto\ \<bb\ 12\>;\ [INV]\l\ +else\l\ +\ \ goto\ \<bb\ 13\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_12 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 12\>:\l\ +|__builtin_puts\ (&\"Binary\ search\ successful!\"[0]);\l\ +goto\ \<bb\ 14\>;\ [INV]\l\ +}"]; + + fn_0_basic_block_13 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 13\>:\l\ +|__builtin_puts\ (&\"Search\ failed!\"[0]);\l\ +}"]; + + fn_0_basic_block_14 [shape=record,style=filled,fillcolor=lightgrey,label="{\<bb\ 14\>:\l\ +|n\ =\ \{CLOBBER(eol)\};\l\ +|search\ =\ \{CLOBBER(eol)\};\l\ +|array\ =\ \{CLOBBER(eol)\};\l\ +|return;\l\ +}"]; + + fn_0_basic_block_0:s -> fn_0_basic_block_2:n [style="solid,bold",color=black,weight=100,constraint=true]; + fn_0_basic_block_2:s -> fn_0_basic_block_4:n [style="solid,bold",color=black,weight=100,constraint=true]; + fn_0_basic_block_3:s -> fn_0_basic_block_4:n [style="dotted,bold",color=blue,weight=10,constraint=false]; + fn_0_basic_block_4:s -> fn_0_basic_block_3:n [style="solid,bold",color=forestgreen,weight=10,constraint=true]; + fn_0_basic_block_4:s -> fn_0_basic_block_5:n [style="solid,bold",color=darkorange,weight=10,constraint=true]; + fn_0_basic_block_5:s -> fn_0_basic_block_9:n [style="solid,bold",color=black,weight=100,constraint=true]; + fn_0_basic_block_6:s -> fn_0_basic_block_7:n [style="solid,bold",color=forestgreen,weight=10,constraint=true]; + fn_0_basic_block_6:s -> fn_0_basic_block_8:n [style="solid,bold",color=darkorange,weight=10,constraint=true]; + fn_0_basic_block_7:s -> fn_0_basic_block_9:n [style="dotted,bold",color=blue,weight=10,constraint=false]; + fn_0_basic_block_8:s -> fn_0_basic_block_9:n [style="dotted,bold",color=blue,weight=10,constraint=false]; + fn_0_basic_block_9:s -> fn_0_basic_block_10:n [style="solid,bold",color=forestgreen,weight=10,constraint=true]; + fn_0_basic_block_9:s -> fn_0_basic_block_11:n [style="solid,bold",color=darkorange,weight=10,constraint=true]; + fn_0_basic_block_10:s -> fn_0_basic_block_6:n [style="solid,bold",color=forestgreen,weight=10,constraint=true]; + fn_0_basic_block_10:s -> fn_0_basic_block_11:n [style="solid,bold",color=darkorange,weight=10,constraint=true]; + fn_0_basic_block_11:s -> fn_0_basic_block_12:n [style="solid,bold",color=forestgreen,weight=10,constraint=true]; + fn_0_basic_block_11:s -> fn_0_basic_block_13:n [style="solid,bold",color=darkorange,weight=10,constraint=true]; + fn_0_basic_block_12:s -> fn_0_basic_block_14:n [style="solid,bold",color=black,weight=100,constraint=true]; + fn_0_basic_block_13:s -> fn_0_basic_block_14:n [style="solid,bold",color=black,weight=100,constraint=true]; + fn_0_basic_block_14:s -> fn_0_basic_block_1:n [style="solid,bold",color=black,weight=10,constraint=true]; + fn_0_basic_block_0:s -> fn_0_basic_block_1:n [style="invis",constraint=true]; +} +} diff --git a/exemplos_codigos/imagens/c/2.png b/exemplos_codigos/imagens/c/2.png new file mode 100644 index 0000000000000000000000000000000000000000..9df67456462ecbd350f40be3e94cc923fb3e7d7e Binary files /dev/null and b/exemplos_codigos/imagens/c/2.png differ diff --git a/exemplos_codigos/imagens/python/2.pycfg.dot b/exemplos_codigos/imagens/python/2.pycfg.dot new file mode 100644 index 0000000000000000000000000000000000000000..cfd027a91fa061dc6b9f392bf5e0890a89776dbe --- /dev/null +++ b/exemplos_codigos/imagens/python/2.pycfg.dot @@ -0,0 +1,50 @@ +strict digraph "" { + node [label="\N"]; + 0 [label="0: start"]; + 21 [label="0: stop"]; + 0 -> 21; + 1 [label="1: enter: main()"]; + 3 [label="2: array = ([0] * 100)"]; + 1 -> 3; + 2 [label="1: exit: main()"]; + 20 [label="26: return 0"]; + 20 -> 2; + 4 [label="3: n = int(input('Enter number of elements: '))"]; + 3 -> 4; + 5 [label="4: c = 0"]; + 4 -> 5; + 6 [label="6: while: (c < n)"]; + 5 -> 6; + 7 [label="7: array[c] = int(input(f'Enter element {(c + 1)}: '))"]; + 6 -> 7; + 8 [label="10: search = int(input('Enter value to find: '))"]; + 6 -> 8; + 7 -> 6; + 9 [label="11: first = 0"]; + 8 -> 9; + 10 [label="12: last = (n - 1)"]; + 9 -> 10; + 11 [label="13: middle = ((first + last) // 2)"]; + 10 -> 11; + 12 [label="15: while: ((array[middle] != search) and (first <= last))"]; + 11 -> 12; + 13 [label="16: if: (array[middle] < search)"]; + 12 -> 13; + 17 [label="22: if: ((first <= last) and (array[middle] == search))"]; + 12 -> 17; + 16 [label="20: middle = ((first + last) // 2)"]; + 16 -> 12; + 14 [label="17: first = (middle + 1)"]; + 13 -> 14; + 15 [label="19: last = (middle - 1)"]; + 13 -> 15; + 14 -> 16; + 15 -> 16; + 18 [label="23: print('Binary search successful!')"]; + 17 -> 18; + 19 [label="25: print('Search failed!')"]; + 17 -> 19; + 18 -> 20; + 19 -> 20; +} + diff --git a/exemplos_codigos/imagens/python/2.pycfg.png b/exemplos_codigos/imagens/python/2.pycfg.png new file mode 100644 index 0000000000000000000000000000000000000000..a1dcd15f53c6cc928f1eb72a5137620564490a91 Binary files /dev/null and b/exemplos_codigos/imagens/python/2.pycfg.png differ diff --git a/exemplos_codigos/imagens/python/2.staticfg.dot b/exemplos_codigos/imagens/python/2.staticfg.dot new file mode 100644 index 0000000000000000000000000000000000000000..b541ff191d6d8e34da40f410f42c324f189d2bbb --- /dev/null +++ b/exemplos_codigos/imagens/python/2.staticfg.dot @@ -0,0 +1,66 @@ +digraph "cluster2.py" { + graph [label="2.py"] + 1 [label="def main():... +"] + subgraph clustermain { + graph [label=main] + 3 [label="array = [0] * 100 +n = int(input('Enter number of elements: ')) +c = 0 +"] + "3_calls" [label=int shape=box] + 3 -> "3_calls" [label=calls style=dashed] + 4 [label="while c < n: +"] + 5 [label="array[c] = int(input(f'Enter element {c + 1}: ')) +c += 1 +"] + "5_calls" [label=int shape=box] + 5 -> "5_calls" [label=calls style=dashed] + 5 -> 4 [label=""] + 4 -> 5 [label="c < n"] + 6 [label="search = int(input('Enter value to find: ')) +first = 0 +last = n - 1 +middle = (first + last) // 2 +"] + "6_calls" [label=int shape=box] + 6 -> "6_calls" [label=calls style=dashed] + 7 [label="while array[middle] != search and first <= last: +"] + 8 [label="if array[middle] < search: +"] + 10 [label="first = middle + 1 +"] + 11 [label="middle = (first + last) // 2 +"] + 11 -> 7 [label=""] + 10 -> 11 [label=""] + 8 -> 10 [label="array[middle] < search"] + 12 [label="last = middle - 1 +"] + 12 -> 11 [label=""] + 8 -> 12 [label="(array[middle] >= search)"] + 7 -> 8 [label="array[middle] != search and first <= last"] + 9 [label="if first <= last and array[middle] == search: +"] + 13 [label="print('Binary search successful!') +"] + "13_calls" [label=print shape=box] + 13 -> "13_calls" [label=calls style=dashed] + 14 [label="return 0 +"] + 13 -> 14 [label=""] + 9 -> 13 [label="first <= last and array[middle] == search"] + 15 [label="print('Search failed!') +"] + "15_calls" [label=print shape=box] + 15 -> "15_calls" [label=calls style=dashed] + 15 -> 14 [label=""] + 9 -> 15 [label="(not (first <= last and array[middle] == search))"] + 7 -> 9 [label="(not (array[middle] != search and first <= last))"] + 6 -> 7 [label=""] + 4 -> 6 [label="(c >= n)"] + 3 -> 4 [label=""] + } +} diff --git a/exemplos_codigos/imagens/python/2.staticfg.pdf b/exemplos_codigos/imagens/python/2.staticfg.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6f13f5fab6c74880701be4ff39a591a81dbe8d04 Binary files /dev/null and b/exemplos_codigos/imagens/python/2.staticfg.pdf differ diff --git a/src/dotmatrix/README.md b/src/dotmatrix/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0e5db53bd89eedee8389c8ff1ea979f117a4566f --- /dev/null +++ b/src/dotmatrix/README.md @@ -0,0 +1,17 @@ +# DOTMATRIX + +This converts a DOT file to an adjacency matrix. + +#### Example usage: +First, include the dotmatrix module. +Then, call ```dotmatrix.convert_dot_file_to_matrix```. The first argument is the path to the target dot file, and the second argument is a callback function to process the results. + +e.g. +``` +const dotmatrix = require('./dotmatrix.js'); +const dot_path = process.argv[2]; + +dotmatrix.convert_dot_file_to_matrix(dot_path, (matrix) => { + console.log(matrix); +}); +``` diff --git a/src/dotmatrix/dotmatrix.js b/src/dotmatrix/dotmatrix.js new file mode 100644 index 0000000000000000000000000000000000000000..0b1b871394431cf72d748e0d666aaef8763744b8 --- /dev/null +++ b/src/dotmatrix/dotmatrix.js @@ -0,0 +1,87 @@ +const fs = require('fs'); +const readline = require('readline'); + +module.exports = (function () { + this.edges = new Array(); + this.adjacency_list = {}; + + function convert_edges_to_adjacency_list(edges) { + edges.sort(); + const vertices = new Array(); + + for (let i = 0; i < edges.length; i++) { + const vertex_left = edges[i][0].replace(';', ''); + const vertex_right = edges[i][1].replace(';', ''); + + if (!vertices.includes(vertex_left)) { + vertices.push(vertex_left); + } + if (!vertices.includes(vertex_right)) { + vertices.push(vertex_right); + } + + if (this.adjacency_list[vertex_left] == undefined) { + this.adjacency_list[vertex_left] = new Array(vertex_right); + } else { + this.adjacency_list[vertex_left].push(vertex_right); + } + + if (this.adjacency_list[vertex_right] == undefined) { + this.adjacency_list[vertex_right] = new Array(); + } + } + + vertices.sort(); + return vertices; + } + + function convert_list_to_matrix(list) { + const num_unique_vertices = list.length; + const adjacency_matrix = new Array(num_unique_vertices); + for (let i = 0; i < num_unique_vertices; i++) { + adjacency_matrix[i] = new Array(num_unique_vertices); + } + + for (let i = 0; i < num_unique_vertices; i++) { + for (let j = 0; j < num_unique_vertices; j++) { + adjacency_matrix[i][j] = 0; + } + } + + for (let i = 0; i < num_unique_vertices; i++) { + const current_vertex = list[i]; + this.adjacency_list[current_vertex].forEach((vertex) => { + adjacency_matrix[i][list.indexOf(vertex)] = 1; + }); + process.stdout.write(i + '\r'); + } + return adjacency_matrix; + } + + function convert_dot_file_to_matrix(path, callback) { + this.read_stream = fs.createReadStream(path).setEncoding('ascii'); + + const rl = readline.createInterface({ + input: this.read_stream + }); + + rl.on('line', (line) => { + if (line.includes('->')) { + edges.push(line.split(' -> ')); + } + if (line.includes('--')) { + edges.push(line.split(' -- ')); + } + }); + + rl.on('close', () => { + const vertices = convert_edges_to_adjacency_list(edges); + const adjacency_matrix = convert_list_to_matrix(vertices); + callback(adjacency_matrix); + }); + } + + return { + convert_dot_file_to_matrix: convert_dot_file_to_matrix + }; +}()); diff --git a/src/dotmatrix/dotmatrix_example.js b/src/dotmatrix/dotmatrix_example.js new file mode 100644 index 0000000000000000000000000000000000000000..ec0a69184f7f6f5f8a5a1427b45e40c97156c2da --- /dev/null +++ b/src/dotmatrix/dotmatrix_example.js @@ -0,0 +1,6 @@ +const dotmatrix = require('./dotmatrix.js'); +const dot_path = process.argv[2]; + +dotmatrix.convert_dot_file_to_matrix(dot_path, (matrix) => { + console.log(matrix); +}); diff --git a/src/dotmatrix/package.json b/src/dotmatrix/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6cda197159d966fe428560ae92054b82bd577953 --- /dev/null +++ b/src/dotmatrix/package.json @@ -0,0 +1,25 @@ +{ + "name": "dotmatrix", + "version": "0.1.0", + "description": "Converts a DOT file to an adjacency matrix", + "main": "dotmatrix.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pmusgrave/dotmatrix.git" + }, + "keywords": [ + "dot", + "gv", + "adjacency", + "matrix" + ], + "author": "Paul Musgrave", + "license": "ISC", + "bugs": { + "url": "https://github.com/pmusgrave/dotmatrix/issues" + }, + "homepage": "https://github.com/pmusgrave/dotmatrix#readme" +} diff --git a/src/dotmatrix/test.dot b/src/dotmatrix/test.dot new file mode 100644 index 0000000000000000000000000000000000000000..d06840bfbac20a0daed9fe0069e116da1796f492 --- /dev/null +++ b/src/dotmatrix/test.dot @@ -0,0 +1,10 @@ +a -- b +a -- d +b -- c +b -- e +e -- d +a -- b +a -- d +b -- c +b -- e +e -- d diff --git a/src/staticfg/.gitignore b/src/staticfg/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..443872b0388d933cf03fb8e0b957a127e3f037aa --- /dev/null +++ b/src/staticfg/.gitignore @@ -0,0 +1,13 @@ +# Compiled python modules. +*.pyc + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info + +**/.DS_Store + +/build +/\.idea \ No newline at end of file diff --git a/src/staticfg/LICENSE b/src/staticfg/LICENSE new file mode 100755 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/src/staticfg/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/src/staticfg/README.md b/src/staticfg/README.md new file mode 100755 index 0000000000000000000000000000000000000000..2e0f4485e35d3d167503cf56b15e8f9094ccbb50 --- /dev/null +++ b/src/staticfg/README.md @@ -0,0 +1,55 @@ +# StatiCFG +Python3 control flow graph generator + +StatiCFG is a package that can be used to produce control flow graphs (CFGs) for Python 3 programs. The CFGs it generates +can be easily visualised with graphviz and used for static analysis. This analysis is actually the main purpose of +the module, hence the name of **StatiC**FG. + +Below is an example of a piece of code that generates the Fibonacci sequence and the CFG produced for it with StatiCFG. + +```python +def fib(): + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + +fib_gen = fib() +for _ in range(10): + next(fib_gen) +``` + + + +## Installation + +To install StatiCFG, simply clone this repository and run the command `pip3 install --upgrade .` inside of it. Please note that +you will also need to install [Graphviz](https://www.graphviz.org/) on your machine to be able to visualise the control flow +graphs generated by StatiCFG. + +## Usage + +To use StatiCFG, simply import the module in your Python interpreter or program, and use the `staticfg.CFGBuilder` class to +build CFGs. For example, to build the CFG of a program defined in a file with the path *./example.py*, the following code can +be used: + +``` +from staticfg import CFGBuilder + +cfg = CFGBuilder().build_from_file('example.py', './example.py') +``` + +This returns the CFG for the code in *./example.py* in the `cfg` variable. The first parameter of `build_from_file` is the +desired name for the CFG, and the second one is the path to the file containing the source code. The produced CFG can then be +visualised with: + +``` +cfg.build_visual('exampleCFG', 'pdf') +``` + +The first paramter of `build_visual` is the desired name for the DOT file produced by the method, and the second one is the +format to use for the visualisation. + +The *build_cfg.py* script present in the */examples* folder of this repository can be used to directly generate the CFG of some +Python program and visualise it. To do so, simply call the script with the command `python3 build_cfg.py +<path_to_some_source>`. diff --git a/src/staticfg/assets/example_cfg.png b/src/staticfg/assets/example_cfg.png new file mode 100755 index 0000000000000000000000000000000000000000..0ec93bf38ce8d7e065ca3a6ce76033d8c32ddfcc Binary files /dev/null and b/src/staticfg/assets/example_cfg.png differ diff --git a/src/staticfg/examples/build_cfg.py b/src/staticfg/examples/build_cfg.py new file mode 100755 index 0000000000000000000000000000000000000000..a9ba84cb57a7586dc25798e69e3a89d75c2d0f42 --- /dev/null +++ b/src/staticfg/examples/build_cfg.py @@ -0,0 +1,16 @@ +#!usr/bin/python3 + +import argparse +from staticfg import CFGBuilder + +parser = argparse.ArgumentParser(description='Generate the control flow graph\ + of a Python program') +parser.add_argument('input_file', help='Path to a file containing a Python\ + program for which the CFG must be generated') +parser.add_argument('output_file', help='Path to a file where the produced\ + visualisation of the CFG must be saved') + +args = parser.parse_args() +cfg_name = args.input_file.split('/')[-1] +cfg = CFGBuilder().build_from_file(cfg_name, args.input_file) +cfg.build_visual(args.output_file, format='pdf', calls=True) diff --git a/src/staticfg/setup.py b/src/staticfg/setup.py new file mode 100755 index 0000000000000000000000000000000000000000..06dc1bbe892488131fae148a6b1ed0303c4016e8 --- /dev/null +++ b/src/staticfg/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + + +setup(name='staticfg', + version='0.9.6', + url='https://github.com/coetaur0/staticfg', + license='Apache 2', + author='Aurelien Coet', + author_email='aurelien.coet19@gmail.com', + description='Control flow graph generator for Python3 programs', + packages=['staticfg'], + test_suite='tests', + install_requires=[ + 'astor', + 'graphviz', + ]) diff --git a/src/staticfg/staticfg/__init__.py b/src/staticfg/staticfg/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..79807578fe3089bc86dfc2c88642f95fbb446185 --- /dev/null +++ b/src/staticfg/staticfg/__init__.py @@ -0,0 +1,2 @@ +from .builder import CFGBuilder +from .model import Block, Link, CFG diff --git a/src/staticfg/staticfg/builder.py b/src/staticfg/staticfg/builder.py new file mode 100755 index 0000000000000000000000000000000000000000..8779d857d97b37c4f3642247a7018d8d61585c38 --- /dev/null +++ b/src/staticfg/staticfg/builder.py @@ -0,0 +1,460 @@ +""" +Control flow graph builder. +""" +# Aurelien Coet, 2018. +# Modified by Andrei Nacu, 2020 + +import ast +from .model import Block, Link, CFG +import sys + + +def is_py38_or_higher(): + if sys.version_info.major == 3 and sys.version_info.minor >= 8: + return True + return False + + +NAMECONSTANT_TYPE = ast.Constant if is_py38_or_higher() else ast.NameConstant + + +def invert(node): + """ + Invert the operation in an ast node object (get its negation). + + Args: + node: An ast node object. + + Returns: + An ast node object containing the inverse (negation) of the input node. + """ + inverse = {ast.Eq: ast.NotEq, + ast.NotEq: ast.Eq, + ast.Lt: ast.GtE, + ast.LtE: ast.Gt, + ast.Gt: ast.LtE, + ast.GtE: ast.Lt, + ast.Is: ast.IsNot, + ast.IsNot: ast.Is, + ast.In: ast.NotIn, + ast.NotIn: ast.In} + + if type(node) == ast.Compare: + op = type(node.ops[0]) + inverse_node = ast.Compare(left=node.left, ops=[inverse[op]()], + comparators=node.comparators) + elif isinstance(node, ast.BinOp) and type(node.op) in inverse: + op = type(node.op) + inverse_node = ast.BinOp(node.left, inverse[op](), node.right) + elif type(node) == NAMECONSTANT_TYPE and node.value in [True, False]: + inverse_node = NAMECONSTANT_TYPE(value=not node.value) + else: + inverse_node = ast.UnaryOp(op=ast.Not(), operand=node) + + return inverse_node + + +def merge_exitcases(exit1, exit2): + """ + Merge the exitcases of two Links. + + Args: + exit1: The exitcase of a Link object. + exit2: Another exitcase to merge with exit1. + + Returns: + The merged exitcases. + """ + if exit1: + if exit2: + return ast.BoolOp(ast.And(), values=[exit1, exit2]) + return exit1 + return exit2 + + +class CFGBuilder(ast.NodeVisitor): + """ + Control flow graph builder. + + A control flow graph builder is an ast.NodeVisitor that can walk through + a program's AST and iteratively build the corresponding CFG. + """ + + def __init__(self, separate=False): + super().__init__() + self.after_loop_block_stack = [] + self.curr_loop_guard_stack = [] + self.current_block = None + self.separate_node_blocks = separate + + # ---------- CFG building methods ---------- # + def build(self, name, tree, asynchr=False, entry_id=0): + """ + Build a CFG from an AST. + + Args: + name: The name of the CFG being built. + tree: The root of the AST from which the CFG must be built. + async: Boolean indicating whether the CFG being built represents an + asynchronous function or not. When the CFG of a Python + program is being built, it is considered like a synchronous + 'main' function. + entry_id: Value for the id of the entry block of the CFG. + + Returns: + The CFG produced from the AST. + """ + self.cfg = CFG(name, asynchr=asynchr) + # Tracking of the current block while building the CFG. + self.current_id = entry_id + self.current_block = self.new_block() + self.cfg.entryblock = self.current_block + # Actual building of the CFG is done here. + self.visit(tree) + self.clean_cfg(self.cfg.entryblock) + return self.cfg + + def build_from_src(self, name, src): + """ + Build a CFG from some Python source code. + + Args: + name: The name of the CFG being built. + src: A string containing the source code to build the CFG from. + + Returns: + The CFG produced from the source code. + """ + tree = ast.parse(src, mode='exec') + return self.build(name, tree) + + def build_from_file(self, name, filepath): + """ + Build a CFG from some Python source file. + + Args: + name: The name of the CFG being built. + filepath: The path to the file containing the Python source code + to build the CFG from. + + Returns: + The CFG produced from the source file. + """ + with open(filepath, 'r') as src_file: + src = src_file.read() + return self.build_from_src(name, src) + + # ---------- Graph management methods ---------- # + def new_block(self): + """ + Create a new block with a new id. + + Returns: + A Block object with a new unique id. + """ + self.current_id += 1 + return Block(self.current_id) + + def add_statement(self, block, statement): + """ + Add a statement to a block. + + Args: + block: A Block object to which a statement must be added. + statement: An AST node representing the statement that must be + added to the current block. + """ + block.statements.append(statement) + + def add_exit(self, block, nextblock, exitcase=None): + """ + Add a new exit to a block. + + Args: + block: A block to which an exit must be added. + nextblock: The block to which control jumps from the new exit. + exitcase: An AST node representing the 'case' (or condition) + leading to the exit from the block in the program. + """ + newlink = Link(block, nextblock, exitcase) + block.exits.append(newlink) + nextblock.predecessors.append(newlink) + + def new_loopguard(self): + """ + Create a new block for a loop's guard if the current block is not + empty. Links the current block to the new loop guard. + + Returns: + The block to be used as new loop guard. + """ + if (self.current_block.is_empty() and + len(self.current_block.exits) == 0): + # If the current block is empty and has no exits, it is used as + # entry block (condition test) for the loop. + loopguard = self.current_block + else: + # Jump to a new block for the loop's guard if the current block + # isn't empty or has exits. + loopguard = self.new_block() + self.add_exit(self.current_block, loopguard) + return loopguard + + def new_functionCFG(self, node, asynchr=False): + """ + Create a new sub-CFG for a function definition and add it to the + function CFGs of the CFG being built. + + Args: + node: The AST node containing the function definition. + async: Boolean indicating whether the function for which the CFG is + being built is asynchronous or not. + """ + self.current_id += 1 + # A new sub-CFG is created for the body of the function definition and + # added to the function CFGs of the current CFG. + func_body = ast.Module(body=node.body) + func_builder = CFGBuilder() + self.cfg.functioncfgs[node.name] = func_builder.build(node.name, + func_body, + asynchr, + self.current_id) + self.current_id = func_builder.current_id + 1 + + def clean_cfg(self, block, visited=[]): + """ + Remove the useless (empty) blocks from a CFG. + + Args: + block: The block from which to start traversing the CFG to clean + it. + visited: A list of blocks that already have been visited by + clean_cfg (recursive function). + """ + # Don't visit blocks twice. + if block in visited: + return + visited.append(block) + + # Empty blocks are removed from the CFG. + if block.is_empty(): + for pred in block.predecessors: + for exit in block.exits: + self.add_exit(pred.source, exit.target, + merge_exitcases(pred.exitcase, + exit.exitcase)) + # Check if the exit hasn't yet been removed from + # the predecessors of the target block. + if exit in exit.target.predecessors: + exit.target.predecessors.remove(exit) + # Check if the predecessor hasn't yet been removed from + # the exits of the source block. + if pred in pred.source.exits: + pred.source.exits.remove(pred) + + block.predecessors = [] + # as the exits may be modified during the recursive call, it is unsafe to iterate on block.exits + # Created a copy of block.exits before calling clean cfg , and iterate over it instead. + for exit in block.exits[:]: + self.clean_cfg(exit.target, visited) + block.exits = [] + else: + for exit in block.exits[:]: + self.clean_cfg(exit.target, visited) + + # ---------- AST Node visitor methods ---------- # + def goto_new_block(self, node): + if self.separate_node_blocks: + newblock = self.new_block() + self.add_exit(self.current_block, newblock) + self.current_block = newblock + self.generic_visit(node) + + def visit_Expr(self, node): + self.add_statement(self.current_block, node) + self.goto_new_block(node) + + def visit_Call(self, node): + def visit_func(node): + if type(node) == ast.Name: + return node.id + elif type(node) == ast.Attribute: + # Recursion on series of calls to attributes. + func_name = visit_func(node.value) + func_name += "." + node.attr + return func_name + elif type(node) == ast.Str: + return node.s + elif type(node) == ast.Subscript: + return node.value.id + else: + return type(node).__name__ + + func = node.func + func_name = visit_func(func) + self.current_block.func_calls.append(func_name) + + def visit_Assign(self, node): + self.add_statement(self.current_block, node) + self.goto_new_block(node) + + def visit_AnnAssign(self, node): + self.add_statement(self.current_block, node) + self.goto_new_block(node) + + def visit_AugAssign(self, node): + self.add_statement(self.current_block, node) + self.goto_new_block(node) + + def visit_Raise(self, node): + # TODO + pass + + def visit_Assert(self, node): + self.add_statement(self.current_block, node) + # New block for the case in which the assertion 'fails'. + failblock = self.new_block() + self.add_exit(self.current_block, failblock, invert(node.test)) + # If the assertion fails, the current flow ends, so the fail block is a + # final block of the CFG. + self.cfg.finalblocks.append(failblock) + # If the assertion is True, continue the flow of the program. + successblock = self.new_block() + self.add_exit(self.current_block, successblock, node.test) + self.current_block = successblock + self.goto_new_block(node) + + def visit_If(self, node): + # Add the If statement at the end of the current block. + self.add_statement(self.current_block, node) + + # Create a new block for the body of the if. + if_block = self.new_block() + self.add_exit(self.current_block, if_block, node.test) + + # Create a block for the code after the if-else. + afterif_block = self.new_block() + + # New block for the body of the else if there is an else clause. + if len(node.orelse) != 0: + else_block = self.new_block() + self.add_exit(self.current_block, else_block, invert(node.test)) + self.current_block = else_block + # Visit the children in the body of the else to populate the block. + for child in node.orelse: + self.visit(child) + # If encountered a break, exit will have already been added + if not self.current_block.exits: + self.add_exit(self.current_block, afterif_block) + else: + self.add_exit(self.current_block, afterif_block, invert(node.test)) + + # Visit children to populate the if block. + self.current_block = if_block + for child in node.body: + self.visit(child) + if not self.current_block.exits: + self.add_exit(self.current_block, afterif_block) + + # Continue building the CFG in the after-if block. + self.current_block = afterif_block + + def visit_While(self, node): + loop_guard = self.new_loopguard() + self.current_block = loop_guard + self.add_statement(self.current_block, node) + self.curr_loop_guard_stack.append(loop_guard) + # New block for the case where the test in the while is True. + while_block = self.new_block() + self.add_exit(self.current_block, while_block, node.test) + + # New block for the case where the test in the while is False. + afterwhile_block = self.new_block() + self.after_loop_block_stack.append(afterwhile_block) + inverted_test = invert(node.test) + # Skip shortcut loop edge if while True: + if not (isinstance(inverted_test, NAMECONSTANT_TYPE) and + inverted_test.value is False): + self.add_exit(self.current_block, afterwhile_block, inverted_test) + + # Populate the while block. + self.current_block = while_block + for child in node.body: + self.visit(child) + if not self.current_block.exits: + # Did not encounter a break statement, loop back + self.add_exit(self.current_block, loop_guard) + + # Continue building the CFG in the after-while block. + self.current_block = afterwhile_block + self.after_loop_block_stack.pop() + self.curr_loop_guard_stack.pop() + + def visit_For(self, node): + loop_guard = self.new_loopguard() + self.current_block = loop_guard + self.add_statement(self.current_block, node) + self.curr_loop_guard_stack.append(loop_guard) + # New block for the body of the for-loop. + for_block = self.new_block() + self.add_exit(self.current_block, for_block, node.iter) + + # Block of code after the for loop. + afterfor_block = self.new_block() + self.add_exit(self.current_block, afterfor_block) + self.after_loop_block_stack.append(afterfor_block) + self.current_block = for_block + + # Populate the body of the for loop. + for child in node.body: + self.visit(child) + if not self.current_block.exits: + # Did not encounter a break + self.add_exit(self.current_block, loop_guard) + + # Continue building the CFG in the after-for block. + self.current_block = afterfor_block + # Popping the current after loop stack,taking care of errors in case of nested for loops + self.after_loop_block_stack.pop() + self.curr_loop_guard_stack.pop() + + def visit_Break(self, node): + assert len(self.after_loop_block_stack), "Found break not inside loop" + self.add_exit(self.current_block, self.after_loop_block_stack[-1]) + + def visit_Continue(self, node): + assert len(self.curr_loop_guard_stack), "Found continue outside loop" + self.add_exit(self.current_block, self.curr_loop_guard_stack[-1]) + + def visit_Import(self, node): + self.add_statement(self.current_block, node) + + def visit_ImportFrom(self, node): + self.add_statement(self.current_block, node) + + def visit_FunctionDef(self, node): + self.add_statement(self.current_block, node) + self.new_functionCFG(node, asynchr=False) + + def visit_AsyncFunctionDef(self, node): + self.add_statement(self.current_block, node) + self.new_functionCFG(node, asynchr=True) + + def visit_Await(self, node): + afterawait_block = self.new_block() + self.add_exit(self.current_block, afterawait_block) + self.goto_new_block(node) + self.current_block = afterawait_block + + def visit_Return(self, node): + self.add_statement(self.current_block, node) + self.cfg.finalblocks.append(self.current_block) + # Continue in a new block but without any jump to it -> all code after + # the return statement will not be included in the CFG. + self.current_block = self.new_block() + + def visit_Yield(self, node): + self.cfg.asynchr = True + afteryield_block = self.new_block() + self.add_exit(self.current_block, afteryield_block) + self.current_block = afteryield_block diff --git a/src/staticfg/staticfg/model.py b/src/staticfg/staticfg/model.py new file mode 100755 index 0000000000000000000000000000000000000000..4dd139fa338c4c3221605aaebdb34b264a988e04 --- /dev/null +++ b/src/staticfg/staticfg/model.py @@ -0,0 +1,241 @@ +""" +Control flow graph for Python programs. +""" +# Aurelien Coet, 2018. + +import ast +import astor +import graphviz as gv + + +class Block(object): + """ + Basic block in a control flow graph. + + Contains a list of statements executed in a program without any control + jumps. A block of statements is exited through one of its exits. Exits are + a list of Links that represent control flow jumps. + """ + + __slots__ = ["id", "statements", "func_calls", "predecessors", "exits"] + + def __init__(self, id): + # Id of the block. + self.id = id + # Statements in the block. + self.statements = [] + # Calls to functions inside the block (represents context switches to + # some functions' CFGs). + self.func_calls = [] + # Links to predecessors in a control flow graph. + self.predecessors = [] + # Links to the next blocks in a control flow graph. + self.exits = [] + + def __str__(self): + if self.statements: + return "block:{}@{}".format(self.id, self.at()) + return "empty block:{}".format(self.id) + + def __repr__(self): + txt = "{} with {} exits".format(str(self), len(self.exits)) + if self.statements: + txt += ", body=[" + txt += ", ".join([ast.dump(node) for node in self.statements]) + txt += "]" + return txt + + def at(self): + """ + Get the line number of the first statement of the block in the program. + """ + if self.statements and self.statements[0].lineno >= 0: + return self.statements[0].lineno + return None + + def is_empty(self): + """ + Check if the block is empty. + + Returns: + A boolean indicating if the block is empty (True) or not (False). + """ + return len(self.statements) == 0 + + def get_source(self): + """ + Get a string containing the Python source code corresponding to the + statements in the block. + + Returns: + A string containing the source code of the statements. + """ + src = "" + for statement in self.statements: + if type(statement) in [ast.If, ast.For, ast.While]: + src += (astor.to_source(statement)).split('\n')[0] + "\n" + elif type(statement) == ast.FunctionDef or\ + type(statement) == ast.AsyncFunctionDef: + src += (astor.to_source(statement)).split('\n')[0] + "...\n" + else: + src += astor.to_source(statement) + return src + + def get_calls(self): + """ + Get a string containing the calls to other functions inside the block. + + Returns: + A string containing the names of the functions called inside the + block. + """ + txt = "" + for func_name in self.func_calls: + txt += func_name + '\n' + return txt + + +class Link(object): + """ + Link between blocks in a control flow graph. + + Represents a control flow jump between two blocks. Contains an exitcase in + the form of an expression, representing the case in which the associated + control jump is made. + """ + + __slots__ = ["source", "target", "exitcase"] + + def __init__(self, source, target, exitcase=None): + assert type(source) == Block, "Source of a link must be a block" + assert type(target) == Block, "Target of a link must be a block" + # Block from which the control flow jump was made. + self.source = source + # Target block of the control flow jump. + self.target = target + # 'Case' leading to a control flow jump through this link. + self.exitcase = exitcase + + def __str__(self): + return "link from {} to {}".format(str(self.source), str(self.target)) + + def __repr__(self): + if self.exitcase is not None: + return "{}, with exitcase {}".format(str(self), + ast.dump(self.exitcase)) + return str(self) + + def get_exitcase(self): + """ + Get a string containing the Python source code corresponding to the + exitcase of the Link. + + Returns: + A string containing the source code. + """ + if self.exitcase: + return astor.to_source(self.exitcase) + return "" + + +class CFG(object): + """ + Control flow graph (CFG). + + A control flow graph is composed of basic blocks and links between them + representing control flow jumps. It has a unique entry block and several + possible 'final' blocks (blocks with no exits representing the end of the + CFG). + """ + + def __init__(self, name, asynchr=False): + assert type(name) == str, "Name of a CFG must be a string" + assert type(asynchr) == bool, "Async must be a boolean value" + # Name of the function or module being represented. + self.name = name + # Type of function represented by the CFG (sync or async). A Python + # program is considered as a synchronous function (main). + self.asynchr = asynchr + # Entry block of the CFG. + self.entryblock = None + # Final blocks of the CFG. + self.finalblocks = [] + # Sub-CFGs for functions defined inside the current CFG. + self.functioncfgs = {} + + def __str__(self): + return "CFG for {}".format(self.name) + + def _visit_blocks(self, graph, block, visited=[], calls=True): + # Don't visit blocks twice. + if block.id in visited: + return + + nodelabel = block.get_source() + + graph.node(str(block.id), label=nodelabel) + visited.append(block.id) + + # Show the block's function calls in a node. + if calls and block.func_calls: + calls_node = str(block.id)+"_calls" + calls_label = block.get_calls().strip() + graph.node(calls_node, label=calls_label, + _attributes={'shape': 'box'}) + graph.edge(str(block.id), calls_node, label="calls", + _attributes={'style': 'dashed'}) + + # Recursively visit all the blocks of the CFG. + for exit in block.exits: + self._visit_blocks(graph, exit.target, visited, calls=calls) + edgelabel = exit.get_exitcase().strip() + graph.edge(str(block.id), str(exit.target.id), label=edgelabel) + + def _build_visual(self, format='pdf', calls=True): + graph = gv.Digraph(name='cluster'+self.name, format=format, + graph_attr={'label': self.name}) + self._visit_blocks(graph, self.entryblock, visited=[], calls=calls) + + # Build the subgraphs for the function definitions in the CFG and add + # them to the graph. + for subcfg in self.functioncfgs: + subgraph = self.functioncfgs[subcfg]._build_visual(format=format, + calls=calls) + graph.subgraph(subgraph) + + return graph + + def build_visual(self, filepath, format, calls=True, show=True): + """ + Build a visualisation of the CFG with graphviz and output it in a DOT + file. + + Args: + filename: The name of the output file in which the visualisation + must be saved. + format: The format to use for the output file (PDF, ...). + show: A boolean indicating whether to automatically open the output + file after building the visualisation. + """ + graph = self._build_visual(format, calls) + graph.render(filepath, view=show) + + def __iter__(self): + """ + Generator that yields all the blocks in the current graph, then + recursively yields from any sub graphs + """ + visited = set() + to_visit = [self.entryblock] + + while to_visit: + block = to_visit.pop(0) + visited.add(block) + for exit_ in block.exits: + if exit_.target in visited or exit_.target in to_visit: + continue + to_visit.append(exit_.target) + yield block + + for subcfg in self.functioncfgs.values(): + yield from subcfg \ No newline at end of file diff --git a/src/staticfg/teste.png b/src/staticfg/teste.png new file mode 100644 index 0000000000000000000000000000000000000000..07c638167850a826f0ad3b168ef0b294fe4ee7c9 Binary files /dev/null and b/src/staticfg/teste.png differ diff --git a/src/staticfg/tests/test_model.py b/src/staticfg/tests/test_model.py new file mode 100755 index 0000000000000000000000000000000000000000..4950a47c53f133dc8027c10143dfb352d029cfc8 --- /dev/null +++ b/src/staticfg/tests/test_model.py @@ -0,0 +1,194 @@ +import unittest +import ast +from staticfg.model import * +from staticfg.builder import CFGBuilder, is_py38_or_higher + + +class TestBlock(unittest.TestCase): + + def test_instanciation(self): + block = Block(1) + self.assertEqual(block.id, 1) + self.assertEqual(block.statements, []) + self.assertEqual(block.func_calls, []) + self.assertEqual(block.predecessors, []) + self.assertEqual(block.exits, []) + + def test_str_representation(self): + block = Block(1) + self.assertEqual(str(block), "empty block:1") + tree = ast.parse("a = 1") + block.statements.append(tree.body[0]) + self.assertEqual(str(block), "block:1@1") + + def test_repr(self): + block = Block(1) + self.assertEqual(repr(block), "empty block:1 with 0 exits") + tree = ast.parse("a = 1") + block.statements.append(tree.body[0]) + if is_py38_or_higher(): + self.assertEqual(repr(block), "block:1@1 with 0 exits, body=[\ +Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=1, kind=None), type_comment=None)]") + else: + self.assertEqual(repr(block), "block:1@1 with 0 exits, body=[\ +Assign(targets=[Name(id='a', ctx=Store())], value=Num(n=1))]") + + def test_at(self): + block = Block(1) + self.assertEqual(block.at(), None) + tree = ast.parse("a = 1") + block.statements.append(tree.body[0]) + self.assertEqual(block.at(), 1) + + def test_is_empty(self): + block = Block(1) + self.assertTrue(block.is_empty()) + tree = ast.parse("a = 1") + block.statements.append(tree.body[0]) + self.assertFalse(block.is_empty()) + + def test_get_source(self): + block = Block(1) + self.assertEqual(block.get_source(), "") + tree = ast.parse("a = 1") + block.statements.append(tree.body[0]) + self.assertEqual(block.get_source(), "a = 1\n") + + def test_get_calls(self): + block = Block(1) + self.assertEqual(block.get_calls(), "") + block.func_calls.append("fun") + self.assertEqual(block.get_calls(), "fun\n") + + +class TestLink(unittest.TestCase): + + def test_instanciation(self): + block1 = Block(1) + block2 = Block(2) + with self.assertRaises(AssertionError): + Link(2, block2) # Source of a link must be a block. + Link(block1, 2) # Target of a link must be a block. + + condition = ast.parse("a == 1").body[0] + link = Link(block1, block2, condition) + self.assertEqual(link.source, block1) + self.assertEqual(link.target, block2) + self.assertEqual(link.exitcase, condition) + + def test_str_representation(self): + block1 = Block(1) + block2 = Block(2) + link = Link(block1, block2) + self.assertEqual(str(link), "link from empty block:1 to empty block:2") + + def test_repr(self): + block1 = Block(1) + block2 = Block(2) + condition = ast.parse("a == 1").body[0] + link = Link(block1, block2, condition) + self.assertEqual(repr(link), "link from empty block:1 to empty block:2\ +, with exitcase {}".format(ast.dump(condition))) + + def test_get_exitcase(self): + block1 = Block(1) + block2 = Block(2) + condition = ast.parse("a == 1").body[0] + link = Link(block1, block2, condition) + self.assertEqual(link.get_exitcase(), "a == 1\n") + + +class TestCFG(unittest.TestCase): + + def test_instanciation(self): + with self.assertRaises(AssertionError): + CFG(2, False) # Name of a CFG must be a string. + CFG('cfg', 2) # Async argument must be a boolean. + + cfg = CFG('cfg', False) + self.assertEqual(cfg.name, 'cfg') + self.assertFalse(cfg.asynchr) + self.assertEqual(cfg.entryblock, None) + self.assertEqual(cfg.finalblocks, []) + self.assertEqual(cfg.functioncfgs, {}) + + def test_str_representation(self): + cfg = CFG('cfg', False) + self.assertEqual(str(cfg), 'CFG for cfg') + + def test_iter(self): + src = """\ +def fib(): + a, b = 0, 1 + while True: + yield a + a, b = b, a + b +""" + cfg = CFGBuilder().build_from_src("fib", src) + expected_block_sources = [ + "def fib():...\n", + "a, b = 0, 1\n", + "while True:\n", + "yield a\n", + "a, b = b, a + b\n" + ] + for actual_block, expected_src in zip(cfg, expected_block_sources): + self.assertEqual(actual_block.get_source(), expected_src) + + def test_break(self): + src = """\ +def foo(): + i = 0 + while True: + i += 1 + if i == 3: + break + for j in range(3): + i += j + if j == 2: + break + return i +""" + cfg = CFGBuilder().build_from_src("foo", src) + expected_block_sources = [ + "def foo():...\n", + "i = 0\n", + "while True:\n", + "i += 1\n" + "if i == 3:\n", + "for j in range(3):\n", + "i += j\n" + "if j == 2:\n", + "return i\n", + "", + ] + for actual_block, expected_src in zip(cfg, expected_block_sources): + self.assertEqual(actual_block.get_source(), expected_src) + + def test_break_in_main_body(self): + src = """\ +def foo(): + i = 0 + while True: + i += 1 + break + for j in range(3): + i += j + break + return i +""" + cfg = CFGBuilder().build_from_src("foo", src) + expected_block_sources = [ + "def foo():...\n", + "i = 0\n", + "while True:\n", + "i += 1\n", + "for j in range(3):\n", + "i += j\n", + "return i\n" + ] + for actual_block, expected_src in zip(cfg, expected_block_sources): + self.assertEqual(actual_block.get_source(), expected_src) + +if __name__ == "__main__": + unittest.main()