Skip to content

Commit 37425f6

Browse files
authored
Merge pull request #20 from matsoftware/issue-19
Fix for Issue 19
2 parents adf010d + 21f54ff commit 37425f6

File tree

12 files changed

+108
-59
lines changed

12 files changed

+108
-59
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## \[Unreleased]
7+
## [1.4.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.4.0) - 2020-02-25
88

99
### Added
1010

@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- [PR-11](https://github.com/matsoftware/swift-code-metrics/pull/11) Improved layout of bar plots for codebases with many frameworks
1616
- [Issue-12](https://github.com/matsoftware/swift-code-metrics/issues/12) `matplotlib` not initialized if `generate-graphs` is not passed
17-
17+
- [Issue-19](https://github.com/matsoftware/swift-code-metrics/issues/19) Correctly parsing `@testable` imports and fix for test targets incorrectly counted in the `Fan-In` metric
1818

1919
## [1.3.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.3.0) - 2019-03-14
2020

swift_code_metrics/_helpers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import json
44
from typing import Dict
5-
5+
from functional import seq
66

77
class Log:
88
__logger = logging.getLogger(__name__)
@@ -206,7 +206,7 @@ class ParsingHelpers:
206206
END_COMMENT = '\*/$'
207207
SINGLE_COMMENT = '^//'
208208

209-
IMPORTS = '(?<=^import )(?:\\b\w+\s|)([^.; ]+)'
209+
IMPORTS = '(?:(?<=^import )|@testable import )(?:\\b\w+\s|)([^.; ]+)'
210210

211211
PROTOCOLS = '.*protocol (.*?)[:|{|\s]'
212212
STRUCTS = '.*struct (.*?)[:|{|\s]'
@@ -230,6 +230,13 @@ def extract_substring_with_pattern(regex_pattern, trimmed_string):
230230
except AttributeError:
231231
return ''
232232

233+
@staticmethod
234+
def reduce_dictionary(items: Dict[str, int]) -> int:
235+
if len(items.values()) == 0:
236+
return 0
237+
return seq(items.values()) \
238+
.reduce(lambda f1, f2: f1 + f2)
239+
233240

234241
class ReportingHelpers:
235242

swift_code_metrics/_metrics.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from ._helpers import AnalyzerHelpers
2-
from ._helpers import Log, ReportingHelpers
1+
from ._helpers import AnalyzerHelpers, Log, ParsingHelpers, ReportingHelpers
32
from ._parser import SwiftFile
43
from functional import seq
54
from typing import Dict, List, Optional
@@ -62,7 +61,7 @@ def fan_in(framework: 'Framework', frameworks: List['Framework']) -> int:
6261
:return: The Fan-In value (int)
6362
"""
6463
fan_in = 0
65-
for f in Metrics.__other_frameworks(framework, frameworks):
64+
for f in Metrics.__other_nontest_frameworks(framework, frameworks):
6665
existing = f.imports.get(framework, 0)
6766
fan_in += existing
6867
return fan_in
@@ -169,9 +168,9 @@ def poc_analysis(poc: float) -> str:
169168
# Internal
170169

171170
@staticmethod
172-
def __other_frameworks(framework: 'Framework', frameworks: List['Framework']) -> List['Framework']:
171+
def __other_nontest_frameworks(framework: 'Framework', frameworks: List['Framework']) -> List['Framework']:
173172
return seq(frameworks) \
174-
.filter(lambda f: f is not framework) \
173+
.filter(lambda f: f is not framework and not f.is_test_framework) \
175174
.list()
176175

177176
@staticmethod
@@ -255,42 +254,42 @@ def as_dict(self) -> Dict:
255254

256255

257256
class Framework:
258-
def __init__(self, name: str):
257+
def __init__(self, name: str, is_test_framework: bool = False):
259258
self.name = name
260259
self.number_of_files = 0
261260
self.data = SyntheticData()
262261
self.__total_imports = {}
263-
self.is_test_framework = False
262+
self.is_test_framework = is_test_framework
264263

265264
def __repr__(self):
266265
return self.name + '(' + str(self.number_of_files) + ' files)'
267266

268267
def append_import(self, framework_import: 'Framework'):
268+
"""
269+
Adds the dependent framework to the list of imported dependencies
270+
:param framework_import: The framework that is being imported
271+
:return:
272+
"""
269273
existing_framework = self.__total_imports.get(framework_import)
270274
if not existing_framework:
271275
self.__total_imports[framework_import] = 1
272276
else:
273277
self.__total_imports[framework_import] += 1
274278

275279
@property
276-
def imports(self):
280+
def imports(self) -> Dict[str, int]:
277281
"""
278282
Returns the list of framework imports without Apple libraries
279283
:return: list of filtered imports
280284
"""
281-
return seq(self.__total_imports.items()) \
282-
.filter(lambda f: f[0].name not in AnalyzerHelpers.APPLE_FRAMEWORKS) \
283-
.dict()
285+
return Framework.__filtered_imports(self.__total_imports.items())
284286

285287
@property
286288
def number_of_imports(self) -> int:
287289
"""
288290
:return: The total number of imports for this framework
289291
"""
290-
if len(self.imports.values()) == 0:
291-
return 0
292-
return seq(self.imports.values()) \
293-
.reduce(lambda f1, f2: f1 + f2)
292+
return ParsingHelpers.reduce_dictionary(self.imports)
294293

295294
@property
296295
def compact_name(self) -> str:
@@ -306,6 +305,12 @@ def compact_name(self) -> str:
306305
def compact_name_description(self) -> str:
307306
return f'{self.compact_name} = {self.name}'
308307

308+
# Static
309+
310+
@staticmethod
311+
def __filtered_imports(items: 'ItemsView') -> Dict[str, int]:
312+
return seq(items).filter(lambda f: f[0].name not in AnalyzerHelpers.APPLE_FRAMEWORKS).dict()
313+
309314

310315
class Dependency:
311316
def __init__(self, name: str, dependent_framework: str, number_of_imports: int = 0):

swift_code_metrics/tests/test_helper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ def test_helpers_funcs_property_modifier_extract_substring_with_pattern_expectTr
156156
string = 'private func funzione(){'
157157
self.assertEqual('funzione', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
158158

159+
def test_helpers_funcs_reduce_dictionary(self):
160+
self.assertEqual(3, _helpers.ParsingHelpers.reduce_dictionary({"one": 1, "two": 2}))
161+
159162

160163
if __name__ == '__main__':
161164
unittest.main()

swift_code_metrics/tests/test_metrics.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ def setUp(self):
2828
def test_representation(self):
2929
self.assertEqual(str(self.framework), 'AwesomeName(0 files)')
3030

31-
def test_compact_name_morethanfourcapitals(self):
31+
def test_compact_name_more_than_four_capitals(self):
3232
test_framework = Framework('FrameworkWithMoreThanFourCapitals')
3333
self.assertEqual('FC', test_framework.compact_name)
3434

35-
def test_compact_name_lessthanfourcapitals(self):
35+
def test_compact_name_less_than_four_capitals(self):
3636
self.assertEqual('AN', self.framework.compact_name)
3737

38-
def test_compact_name_nocapitals(self):
38+
def test_compact_name_no_capitals(self):
3939
test_framework = Framework('nocapitals')
4040
self.assertEqual('n', test_framework.compact_name)
4141

@@ -70,26 +70,30 @@ class MetricsTests(unittest.TestCase):
7070

7171
def setUp(self):
7272
self._generate_mocks()
73+
self._populate_imports()
7374

7475
def _generate_mocks(self):
7576
self.foundation_kit = Framework('FoundationKit')
7677
self.design_kit = Framework('DesignKit')
7778
self.app_layer = Framework('ApplicationLayer')
7879
self.rxswift = Framework('RxSwift')
80+
self.test_design_kit = Framework(name='DesignKitTests', is_test_framework=True)
7981
self.awesome_dependency = Framework('AwesomeDependency')
82+
self.not_linked_framework = Framework('External')
8083
self.frameworks = [
8184
self.foundation_kit,
8285
self.design_kit,
83-
self.app_layer
86+
self.app_layer,
87+
self.test_design_kit
8488
]
8589

86-
def _populate_app_layer_imports(self):
90+
def _populate_imports(self):
8791
self.app_layer.append_import(self.design_kit)
8892
self.app_layer.append_import(self.design_kit)
8993
self.app_layer.append_import(self.foundation_kit)
94+
self.test_design_kit.append_import(self.design_kit)
9095

9196
def test_distance_main_sequence(self):
92-
self._populate_app_layer_imports()
9397
self.app_layer.data.number_of_concrete_data_structures = 7
9498
self.app_layer.data.number_of_interfaces = 2
9599

@@ -100,8 +104,10 @@ def test_distance_main_sequence(self):
100104
def test_instability_no_imports(self):
101105
self.assertEqual(0, Metrics.instability(self.foundation_kit, self.frameworks))
102106

107+
def test_instability_not_linked_framework(self):
108+
self.assertEqual(0, Metrics.instability(self.not_linked_framework, self.frameworks))
109+
103110
def test_instability_imports(self):
104-
self._populate_app_layer_imports()
105111
self.assertAlmostEqual(1.0, Metrics.instability(self.app_layer, self.frameworks))
106112

107113
def test_abstractness_no_concretes(self):
@@ -112,12 +118,13 @@ def test_abstractness_concretes(self):
112118
self.foundation_kit.data.number_of_concrete_data_structures = 4
113119
self.assertEqual(2, Metrics.abstractness(self.foundation_kit))
114120

115-
def test_fan_in(self):
116-
self._populate_app_layer_imports()
121+
def test_fan_in_test_frameworks(self):
117122
self.assertEqual(2, Metrics.fan_in(self.design_kit, self.frameworks))
118123

124+
def test_fan_in_no_test_frameworks(self):
125+
self.assertEqual(1, Metrics.fan_in(self.foundation_kit, self.frameworks))
126+
119127
def test_fan_out(self):
120-
self._populate_app_layer_imports()
121128
self.assertEqual(3, Metrics.fan_out(self.app_layer))
122129

123130
def test_external_dependencies(self):
@@ -134,7 +141,6 @@ def test_external_dependencies(self):
134141
self.assertEqual(design_external_deps, [])
135142

136143
def test_internal_dependencies(self):
137-
self._populate_app_layer_imports()
138144
self.design_kit.append_import(self.foundation_kit)
139145

140146
expected_foundation_internal_deps = []
@@ -165,6 +171,24 @@ def test_poc_valid_loc_noc(self):
165171
def test_poc_invalid_loc_noc(self):
166172
self.assertEqual(0, Metrics.percentage_of_comments(loc=0, noc=0))
167173

174+
def test_ia_analysis_zone_of_pain(self):
175+
self.assertTrue("Zone of Pain" in Metrics.ia_analysis(0.4, 0.4))
176+
177+
def test_ia_analysis_zone_of_uselessness(self):
178+
self.assertTrue("Zone of Uselessness" in Metrics.ia_analysis(0.7, 0.7))
179+
180+
def test_ia_analysis_highly_stable(self):
181+
self.assertTrue("Highly stable component" in Metrics.ia_analysis(0.1, 0.51))
182+
183+
def test_ia_analysis_highly_unstable(self):
184+
self.assertTrue("Highly unstable component" in Metrics.ia_analysis(0.81, 0.49))
185+
186+
def test_ia_analysis_low_abstract(self):
187+
self.assertTrue("Low abstract component" in Metrics.ia_analysis(0.51, 0.1))
188+
189+
def test_ia_analysis_high_abstract(self):
190+
self.assertTrue("High abstract component" in Metrics.ia_analysis(0.49, 0.81))
191+
168192
@property
169193
def __dummy_external_frameworks(self):
170194
return [

swift_code_metrics/tests/test_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ def test_swiftparser_parse_should_return_expected_n_of_comments(self):
3232
self.assertEqual(21, self.example_parsed_file.n_of_comments)
3333

3434
def test_swiftparser_parse_should_return_expected_loc(self):
35-
self.assertEqual(23, self.example_parsed_file.loc)
35+
self.assertEqual(25, self.example_parsed_file.loc)
3636

3737
def test_swiftparser_parse_should_return_expected_imports(self):
38-
self.assertEqual(['Foundation', 'AmazingFramework'], self.example_parsed_file.imports)
38+
self.assertEqual(['Foundation', 'AmazingFramework', 'Helper', 'TestedLibrary'], self.example_parsed_file.imports)
3939

4040
def test_swiftparser_parse_should_return_expected_interfaces(self):
4141
self.assertEqual(['SimpleProtocol', 'UnusedClassProtocol'], self.example_parsed_file.interfaces)

swift_code_metrics/tests/test_resources/ExampleFile.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import Foundation
99
import AmazingFramework
10+
import struct Helper.CoolFunction
11+
@testable import TestedLibrary
1012

1113
// First protocol SimpleProtocol
1214
protocol SimpleProtocol {}

swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic.xcodeproj/project.pbxproj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@
163163
TargetAttributes = {
164164
E576966321176D6600CADE76 = {
165165
CreatedOnToolsVersion = 9.4;
166-
LastSwiftMigration = 1000;
166+
LastSwiftMigration = 1130;
167167
};
168168
E576966C21176D6600CADE76 = {
169169
CreatedOnToolsVersion = 9.4;
170-
LastSwiftMigration = 1000;
170+
LastSwiftMigration = 1130;
171171
};
172172
};
173173
};
@@ -177,6 +177,7 @@
177177
hasScannedForEncodings = 0;
178178
knownRegions = (
179179
en,
180+
Base,
180181
);
181182
mainGroup = E576965A21176D6600CADE76;
182183
productRefGroup = E576966521176D6600CADE76 /* Products */;
@@ -375,7 +376,7 @@
375376
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
376377
SKIP_INSTALL = YES;
377378
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
378-
SWIFT_VERSION = 4.2;
379+
SWIFT_VERSION = 5.0;
379380
TARGETED_DEVICE_FAMILY = "1,2";
380381
};
381382
name = Debug;
@@ -400,7 +401,7 @@
400401
PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogic;
401402
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
402403
SKIP_INSTALL = YES;
403-
SWIFT_VERSION = 4.2;
404+
SWIFT_VERSION = 5.0;
404405
TARGETED_DEVICE_FAMILY = "1,2";
405406
};
406407
name = Release;
@@ -418,7 +419,7 @@
418419
);
419420
PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogicTests;
420421
PRODUCT_NAME = "$(TARGET_NAME)";
421-
SWIFT_VERSION = 4.2;
422+
SWIFT_VERSION = 5.0;
422423
TARGETED_DEVICE_FAMILY = "1,2";
423424
};
424425
name = Debug;
@@ -436,7 +437,7 @@
436437
);
437438
PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogicTests;
438439
PRODUCT_NAME = "$(TARGET_NAME)";
439-
SWIFT_VERSION = 4.2;
440+
SWIFT_VERSION = 5.0;
440441
TARGETED_DEVICE_FAMILY = "1,2";
441442
};
442443
name = Release;

0 commit comments

Comments
 (0)