Skip to content

Commit 4f874e9

Browse files
authored
Merge pull request #25 from matsoftware/analyze-submodules-part1
Analyze submodules (part 1)
2 parents b4da6ec + a4e5987 commit 4f874e9

File tree

8 files changed

+248
-72
lines changed

8 files changed

+248
-72
lines changed

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
matplotlib >= 2
22
adjustText < 1
33
pygraphviz == 1.5
4-
pyfunctional == 1.2
4+
pyfunctional == 1.2
5+
mergedeep == 1.3.0
6+
dataclasses == 0.7.0

swift_code_metrics/_analyzer.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
from ._helpers import AnalyzerHelpers
55
from ._parser import SwiftFileParser, SwiftFile
6-
from ._metrics import Framework, SyntheticData
6+
from ._metrics import Framework
77
from ._report import ReportProcessor
88
from functional import seq
99
from typing import List, Optional
10+
from mergedeep import merge
1011

1112

1213
class Inspector:
@@ -60,8 +61,7 @@ def __analyze_directory(self, directory: str, exclude_paths: List[str], tests_de
6061

6162
def __append_dependency(self, swift_file: 'SwiftFile'):
6263
framework = self.__get_or_create_framework(swift_file.framework_name)
63-
framework.number_of_files += 1
64-
framework.data.append_data(data=SyntheticData(swift_file=swift_file))
64+
Inspector.__add_raw_files(framework=framework, swift_file=swift_file)
6565
# This covers the scenario where a test framework might contain no tests
6666
framework.is_test_framework = swift_file.is_test
6767

@@ -71,6 +71,14 @@ def __append_dependency(self, swift_file: 'SwiftFile'):
7171
imported_framework = Framework(f)
7272
framework.append_import(imported_framework)
7373

74+
@staticmethod
75+
def __add_raw_files(framework: 'Framework', swift_file: 'SwiftFile'):
76+
paths = str(swift_file.path).split('/')
77+
ref_dict = swift_file
78+
for key in reversed(paths):
79+
ref_dict = {key: ref_dict}
80+
merge(framework.raw_files, ref_dict)
81+
7482
def __process_shared_file(self, swift_file: 'SwiftFile', directory: str):
7583
if not swift_file.is_shared:
7684
return
@@ -99,4 +107,3 @@ def __get_framework(self, name: str) -> Optional['Framework']:
99107
if f.name == name:
100108
return f
101109
return None
102-

swift_code_metrics/_helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import re
22
import logging
33
import json
4-
from typing import Dict
4+
from typing import List,Dict
55
from functional import seq
66

7+
78
class Log:
89
__logger = logging.getLogger(__name__)
910

@@ -270,3 +271,13 @@ class JSONReader:
270271
def read_json_file(path: str) -> Dict:
271272
with open(path, 'r') as fp:
272273
return json.load(fp)
274+
275+
276+
# Methods
277+
278+
def flatten_nested_dictionary_values(dictionary) -> List:
279+
for v in dictionary.values():
280+
if isinstance(v, dict):
281+
yield from flatten_nested_dictionary_values(v)
282+
else:
283+
yield v

swift_code_metrics/_metrics.py

Lines changed: 147 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from ._helpers import AnalyzerHelpers, Log, ParsingHelpers, ReportingHelpers
1+
from ._helpers import AnalyzerHelpers, Log, ParsingHelpers, ReportingHelpers, flatten_nested_dictionary_values
22
from ._parser import SwiftFile
3+
from dataclasses import dataclass
34
from functional import seq
45
from typing import Dict, List, Optional
56

@@ -191,31 +192,61 @@ def __is_name_contained_in_list(framework: 'Framework', frameworks: List['Framew
191192
.list()) > 0
192193

193194

195+
@dataclass
194196
class SyntheticData:
195-
def __init__(self, swift_file: Optional['SwiftFile'] = None):
196-
self.loc = 0 if swift_file is None else swift_file.loc
197-
self.noc = 0 if swift_file is None else swift_file.n_of_comments
198-
self.number_of_interfaces = 0 if swift_file is None else len(swift_file.interfaces)
199-
self.number_of_concrete_data_structures = 0 if swift_file is None else \
200-
len(swift_file.structs + swift_file.classes)
201-
self.number_of_methods = 0 if swift_file is None else len(swift_file.methods)
202-
self.number_of_tests = 0 if swift_file is None else len(swift_file.tests)
203-
204-
def append_data(self, data: 'SyntheticData'):
205-
self.loc += data.loc
206-
self.noc += data.noc
207-
self.number_of_interfaces += data.number_of_interfaces
208-
self.number_of_concrete_data_structures += data.number_of_concrete_data_structures
209-
self.number_of_methods += data.number_of_methods
210-
self.number_of_tests += data.number_of_tests
211-
212-
def remove_data(self, data: 'SyntheticData'):
213-
self.loc -= data.loc
214-
self.noc -= data.noc
215-
self.number_of_interfaces -= data.number_of_interfaces
216-
self.number_of_concrete_data_structures -= data.number_of_concrete_data_structures
217-
self.number_of_methods -= data.number_of_methods
218-
self.number_of_tests -= data.number_of_tests
197+
"""
198+
Representation of synthetic code metric data
199+
"""
200+
loc: int = 0
201+
noc: int = 0
202+
number_of_interfaces: int = 0
203+
number_of_concrete_data_structures: int = 0
204+
number_of_methods: int = 0
205+
number_of_tests: int = 0
206+
207+
@classmethod
208+
def from_swift_file(cls, swift_file: Optional['SwiftFile'] = None) -> 'SyntheticData':
209+
return SyntheticData(
210+
loc=0 if swift_file is None else swift_file.loc,
211+
noc=0 if swift_file is None else swift_file.n_of_comments,
212+
number_of_interfaces=0 if swift_file is None else len(swift_file.interfaces),
213+
number_of_concrete_data_structures=0 if swift_file is None else \
214+
len(swift_file.structs + swift_file.classes),
215+
number_of_methods=0 if swift_file is None else len(swift_file.methods),
216+
number_of_tests=0 if swift_file is None else len(swift_file.tests)
217+
)
218+
219+
def __add__(self, data):
220+
"""
221+
Implementation of the `+` operator
222+
:param data: An instance of SyntheticData
223+
:return: a new instance of SyntheticData
224+
"""
225+
return SyntheticData(
226+
loc=self.loc + data.loc,
227+
noc=self.noc + data.noc,
228+
number_of_interfaces=self.number_of_interfaces + data.number_of_interfaces,
229+
number_of_concrete_data_structures=self.number_of_concrete_data_structures
230+
+ data.number_of_concrete_data_structures,
231+
number_of_methods=self.number_of_methods + data.number_of_methods,
232+
number_of_tests=self.number_of_tests + data.number_of_tests
233+
)
234+
235+
def __sub__(self, data):
236+
"""
237+
Implementation of the `-` operator
238+
:param data: An instance of SyntheticData
239+
:return: a new instance of SyntheticData
240+
"""
241+
return SyntheticData(
242+
loc=self.loc - data.loc,
243+
noc=self.noc - data.noc,
244+
number_of_interfaces=self.number_of_interfaces - data.number_of_interfaces,
245+
number_of_concrete_data_structures=self.number_of_concrete_data_structures
246+
- data.number_of_concrete_data_structures,
247+
number_of_methods=self.number_of_methods - data.number_of_methods,
248+
number_of_tests=self.number_of_tests - data.number_of_tests
249+
)
219250

220251
@property
221252
def poc(self) -> float:
@@ -234,30 +265,82 @@ def as_dict(self) -> Dict:
234265
}
235266

236267

268+
@dataclass()
237269
class FrameworkData(SyntheticData):
238-
def __init__(self, swift_file: Optional['SwiftFile'] = None):
239-
super().__init__(swift_file)
240-
self.n_o_i = 0 if swift_file is None else\
241-
len([imp for imp in swift_file.imports if imp not in AnalyzerHelpers.APPLE_FRAMEWORKS])
270+
"""
271+
Enriched synthetic data
272+
"""
273+
n_o_i: int = 0
274+
275+
@classmethod
276+
def from_swift_file(cls, swift_file: Optional['SwiftFile'] = None) -> 'FrameworkData':
277+
sd = SyntheticData.from_swift_file(swift_file=swift_file)
278+
return FrameworkData.__from_sd(sd=sd,
279+
n_o_i=0 if swift_file is None else \
280+
len([imp for imp in swift_file.imports if imp not in \
281+
AnalyzerHelpers.APPLE_FRAMEWORKS]))
282+
283+
def __add__(self, data):
284+
"""
285+
Implementation of the `+` operator
286+
:param data: An instance of FrameworkData
287+
:return: a new instance of FrameworkData
288+
"""
289+
sd = self.__current_sd().__add__(data=data)
290+
return FrameworkData.__from_sd(sd=sd, n_o_i=self.n_o_i + data.n_o_i)
291+
292+
def __sub__(self, data):
293+
"""
294+
Implementation of the `-` operator
295+
:param data: An instance of FrameworkData
296+
:return: a new instance of FrameworkData
297+
"""
298+
sd = self.__current_sd().__sub__(data=data)
299+
return FrameworkData.__from_sd(sd=sd, n_o_i=self.n_o_i - data.n_o_i)
242300

243301
def append_framework(self, f: 'Framework'):
244-
super().append_data(data=f.data)
302+
sd = f.data
303+
self.loc += sd.loc
304+
self.noc += sd.noc
305+
self.number_of_interfaces += sd.number_of_interfaces
306+
self.number_of_concrete_data_structures += sd.number_of_concrete_data_structures
307+
self.number_of_methods += sd.number_of_methods
308+
self.number_of_tests += sd.number_of_tests
245309
self.n_o_i += f.number_of_imports
246310

247-
def remove_data(self, data: 'FrameworkData'):
248-
super().remove_data(data=data)
249-
self.n_o_i -= data.n_o_i
250-
251311
@property
252312
def as_dict(self) -> Dict:
253313
return {**super().as_dict, **{"noi": self.n_o_i}}
254314

315+
# Private
316+
317+
def __current_sd(self) -> 'SyntheticData':
318+
return SyntheticData(
319+
loc=self.loc,
320+
noc=self.noc,
321+
number_of_interfaces=self.number_of_interfaces,
322+
number_of_concrete_data_structures=self.number_of_concrete_data_structures,
323+
number_of_methods=self.number_of_methods,
324+
number_of_tests=self.number_of_tests
325+
)
326+
327+
@classmethod
328+
def __from_sd(cls, sd: 'SyntheticData', n_o_i: int) -> 'FrameworkData':
329+
return FrameworkData(
330+
loc=sd.loc,
331+
noc=sd.noc,
332+
number_of_interfaces=sd.number_of_interfaces,
333+
number_of_concrete_data_structures=sd.number_of_concrete_data_structures,
334+
number_of_methods=sd.number_of_methods,
335+
number_of_tests=sd.number_of_tests,
336+
n_o_i=n_o_i
337+
)
338+
255339

256340
class Framework:
257341
def __init__(self, name: str, is_test_framework: bool = False):
258342
self.name = name
259-
self.number_of_files = 0
260-
self.data = SyntheticData()
343+
self.raw_files = {}
261344
self.__total_imports = {}
262345
self.is_test_framework = is_test_framework
263346

@@ -276,6 +359,25 @@ def append_import(self, framework_import: 'Framework'):
276359
else:
277360
self.__total_imports[framework_import] += 1
278361

362+
@property
363+
def data(self) -> SyntheticData:
364+
"""
365+
The metrics data describing the framework
366+
:return: an instance of SyntheticData
367+
"""
368+
if self.number_of_files == 0:
369+
return SyntheticData()
370+
return seq([SyntheticData.from_swift_file(swift_file=sf) for sf in self.__raw_files_data()]) \
371+
.reduce(lambda sd1, sd2: sd1 + sd2)
372+
373+
@property
374+
def number_of_files(self) -> int:
375+
"""
376+
Number of files in the framework
377+
:return: The total number of files in this framework (int)
378+
"""
379+
return len(self.__raw_files_data())
380+
279381
@property
280382
def imports(self) -> Dict[str, int]:
281383
"""
@@ -311,12 +413,17 @@ def compact_name_description(self) -> str:
311413
def __filtered_imports(items: 'ItemsView') -> Dict[str, int]:
312414
return seq(items).filter(lambda f: f[0].name not in AnalyzerHelpers.APPLE_FRAMEWORKS).dict()
313415

416+
# Private
417+
418+
def __raw_files_data(self) -> List['SwiftFile']:
419+
return list(flatten_nested_dictionary_values(self.raw_files))
420+
314421

422+
@dataclass
315423
class Dependency:
316-
def __init__(self, name: str, dependent_framework: str, number_of_imports: int = 0):
317-
self.name = name
318-
self.dependent_framework = dependent_framework
319-
self.number_of_imports = number_of_imports
424+
name: str
425+
dependent_framework: str
426+
number_of_imports: int = 0
320427

321428
def __eq__(self, other):
322429
return (self.name == other.name) and \
@@ -333,4 +440,3 @@ def compact_repr(self) -> str:
333440
@property
334441
def relationship(self) -> str:
335442
return f'{self.name} > {self.dependent_framework}'
336-

swift_code_metrics/_parser.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77

88
class SwiftFile(object):
9-
def __init__(self, framework_name: List[str],
9+
def __init__(self,
10+
path: str,
11+
framework_name: str,
1012
loc: int,
1113
imports: List[str],
1214
interfaces: List[str],
@@ -18,6 +20,7 @@ def __init__(self, framework_name: List[str],
1820
is_test: bool):
1921
"""
2022
Creates a SwiftFile instance that represents a parsed swift file.
23+
:param path: The path of the file being analyzed
2124
:param framework_name: The framework where the file belongs to.
2225
:param loc: Lines Of Code
2326
:param imports: List of imported frameworks
@@ -29,6 +32,7 @@ def __init__(self, framework_name: List[str],
2932
:param is_shared: True if the file is shared with other frameworks
3033
:param is_test: True if the file is a test class
3134
"""
35+
self.path = path
3236
self.framework_name = framework_name
3337
self.loc = loc
3438
self.imports = imports
@@ -133,6 +137,7 @@ def parse(self) -> List['SwiftFile']:
133137

134138
is_shared_file = len(framework_names) > 1
135139
return [SwiftFile(
140+
path=Path(self.current_subdir.replace(f'{self.base_path}/', '')) / Path(self.file).name,
136141
framework_name=f,
137142
loc=loc,
138143
imports=self.attributes_regex_map[ParsingHelpers.IMPORTS],

swift_code_metrics/_report.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ def generate_report(frameworks: List['Framework'], shared_files: Dict[str, 'Swif
1313
# Shared files
1414
for _, shared_files in shared_files.items():
1515
shared_file = shared_files[0]
16-
shared_file_data = FrameworkData(swift_file=shared_file)
16+
shared_file_data = FrameworkData.from_swift_file(swift_file=shared_file)
1717
for _ in range((len(shared_files) - 1)):
18-
report.shared_code.append_data(shared_file_data)
19-
report.total_aggregate.remove_data(shared_file_data)
18+
report.shared_code += shared_file_data
19+
report.total_aggregate -= shared_file_data
2020
if shared_file.is_test:
21-
report.test_framework_aggregate.remove_data(shared_file_data)
21+
report.test_framework_aggregate -= shared_file_data
2222
else:
23-
report.non_test_framework_aggregate.remove_data(shared_file_data)
23+
report.non_test_framework_aggregate -= shared_file_data
2424

2525
# Frameworks
2626
for f in sorted(frameworks, key=lambda fr: fr.name, reverse=False):

swift_code_metrics/tests/test_helper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ def test_helpers_funcs_property_modifier_extract_substring_with_pattern_expectTr
164164
def test_helpers_funcs_reduce_dictionary(self):
165165
self.assertEqual(3, _helpers.ParsingHelpers.reduce_dictionary({"one": 1, "two": 2}))
166166

167+
# Additional helpers
168+
169+
def test_flatten_nested_dictionary_values(self):
170+
nested_dictionary = {'group1': {'subGroup1': 1}, 'group2': 2, 'group3': {'subGroup3': {'subSubGroup3': 3}}}
171+
flattened_values = list(_helpers.flatten_nested_dictionary_values(nested_dictionary))
172+
self.assertEqual([1, 2, 3], flattened_values)
173+
167174

168175
if __name__ == '__main__':
169176
unittest.main()

0 commit comments

Comments
 (0)