├── requirements.txt
├── MANIFEST.in
├── swift_code_metrics
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_resources
│ │ ├── ExampleProject
│ │ │ └── SwiftCodeMetricsExample
│ │ │ │ ├── SwiftCodeMetricsExample
│ │ │ │ ├── Assets.xcassets
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ │ └── Contents.json
│ │ │ │ ├── AppDelegate.swift
│ │ │ │ ├── ViewController.swift
│ │ │ │ ├── Info.plist
│ │ │ │ └── Base.lproj
│ │ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ │ └── Main.storyboard
│ │ │ │ ├── BusinessLogic
│ │ │ │ ├── BusinessLogic.xcodeproj
│ │ │ │ │ ├── project.xcworkspace
│ │ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ │ └── xcshareddata
│ │ │ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ │ └── project.pbxproj
│ │ │ │ ├── BusinessLogicTests
│ │ │ │ │ ├── AwesomeFeatureViewControllerTests.swift
│ │ │ │ │ └── Info.plist
│ │ │ │ └── BusinessLogic
│ │ │ │ │ ├── Info.plist
│ │ │ │ │ └── AwesomeFeature.swift
│ │ │ │ ├── Foundation
│ │ │ │ ├── Foundation.xcodeproj
│ │ │ │ │ └── project.xcworkspace
│ │ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ │ └── xcshareddata
│ │ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ ├── Shared
│ │ │ │ │ └── Helpers.swift
│ │ │ │ ├── FoundationFrameworkTests
│ │ │ │ │ ├── FoundationFrameworkTests.swift
│ │ │ │ │ ├── Info.plist
│ │ │ │ │ └── NetworkingTests.swift
│ │ │ │ ├── scm.json
│ │ │ │ ├── SecretLib
│ │ │ │ │ ├── CaesarChiper.swift
│ │ │ │ │ └── Info.plist
│ │ │ │ └── FoundationFramework
│ │ │ │ │ ├── Interfaces
│ │ │ │ │ └── CommonTypes.swift
│ │ │ │ │ ├── Networking.swift
│ │ │ │ │ └── Info.plist
│ │ │ │ ├── SwiftCodeMetricsExample.xcodeproj
│ │ │ │ ├── project.xcworkspace
│ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ └── xcshareddata
│ │ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ └── xcshareddata
│ │ │ │ │ └── xcschemes
│ │ │ │ │ └── SwiftCodeMetricsExample.xcscheme
│ │ │ │ └── SwiftCodeMetricsExampleTests
│ │ │ │ ├── SwiftCodeMetricsExampleTests.swift
│ │ │ │ └── Info.plist
│ │ ├── scm_overrides
│ │ │ ├── invalid_scm_override.json
│ │ │ └── valid_scm_override.json
│ │ ├── ExampleTest.swift
│ │ ├── ExampleFile.swift
│ │ └── expected_output.json
│ ├── test_integration.py
│ ├── test_parser.py
│ ├── test_helper.py
│ └── test_metrics.py
├── version.py
├── __main__.py
├── scm.py
├── _graph_helpers.py
├── _graphs_renderer.py
├── _graphs_presenter.py
├── _report.py
├── _analyzer.py
├── _parser.py
├── _helpers.py
└── _metrics.py
├── .github
├── FUNDING.yml
└── workflows
│ └── deploy.yml
├── codecov.sh
├── docs
├── assets
│ ├── code_distribution.jpeg
│ ├── lines_of_code_-_loc.jpeg
│ ├── n._of_imports_-_noi.jpeg
│ ├── scm_override_example.png
│ ├── number_of_tests_-_not.jpeg
│ ├── number_of_comments_-_noc.jpeg
│ ├── n._of_classes_and_structs.jpeg
│ ├── code_distribution_submodules.jpeg
│ ├── example_internal_deps_graph.jpeg
│ └── example_deviation_main_sequence.jpeg
└── GUIDE.md
├── tests-requirements.txt
├── swift-code-metrics-runner.py
├── .travis.yml
├── .coveragerc
├── .vscode
├── settings.json
└── launch.json
├── install.sh
├── LICENSE
├── setup.py
├── .gitignore
├── README.md
└── CHANGELOG.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | .
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
--------------------------------------------------------------------------------
/swift_code_metrics/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/swift_code_metrics/version.py:
--------------------------------------------------------------------------------
1 | VERSION = "1.5.4"
2 |
--------------------------------------------------------------------------------
/swift_code_metrics/__main__.py:
--------------------------------------------------------------------------------
1 | from .scm import main
2 | main()
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | liberapay: matsoftware
4 |
--------------------------------------------------------------------------------
/codecov.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | top="$(dirname "$0")"
6 |
7 | "${top}"/venv/bin/codecov
--------------------------------------------------------------------------------
/docs/assets/code_distribution.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/code_distribution.jpeg
--------------------------------------------------------------------------------
/docs/assets/lines_of_code_-_loc.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/lines_of_code_-_loc.jpeg
--------------------------------------------------------------------------------
/docs/assets/n._of_imports_-_noi.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/n._of_imports_-_noi.jpeg
--------------------------------------------------------------------------------
/docs/assets/scm_override_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/scm_override_example.png
--------------------------------------------------------------------------------
/docs/assets/number_of_tests_-_not.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/number_of_tests_-_not.jpeg
--------------------------------------------------------------------------------
/docs/assets/number_of_comments_-_noc.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/number_of_comments_-_noc.jpeg
--------------------------------------------------------------------------------
/docs/assets/n._of_classes_and_structs.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/n._of_classes_and_structs.jpeg
--------------------------------------------------------------------------------
/docs/assets/code_distribution_submodules.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/code_distribution_submodules.jpeg
--------------------------------------------------------------------------------
/docs/assets/example_internal_deps_graph.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/example_internal_deps_graph.jpeg
--------------------------------------------------------------------------------
/docs/assets/example_deviation_main_sequence.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matsoftware/swift-code-metrics/HEAD/docs/assets/example_deviation_main_sequence.jpeg
--------------------------------------------------------------------------------
/tests-requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-cov
3 | codecov
4 | coverage
5 | urllib3 <=1.26.15 # https://github.com/psf/requests/issues/6432#issuecomment-1535149114
--------------------------------------------------------------------------------
/swift-code-metrics-runner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from swift_code_metrics.scm import main
5 |
6 |
7 | if __name__ == '__main__':
8 | main()
9 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.8"
5 | install:
6 | - ./install.sh
7 | script: ./build_and_test.sh
8 | after_success:
9 | - ./codecov.sh
10 | branches:
11 | only:
12 | - master
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | # .coveragerc
2 | [run]
3 | include=swift_code_metrics/*
4 | omit=
5 | */__init__.py
6 | */__main__.py
7 | */version.py
8 | */scm.py
9 | */tests/*
10 | [report]
11 | show_missing = True
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.testing.pytestArgs": [
3 | "swift_code_metrics"
4 | ],
5 | "python.testing.unittestEnabled": false,
6 | "python.testing.nosetestsEnabled": false,
7 | "python.testing.pytestEnabled": true
8 | }
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/scm_overrides/invalid_scm_override.json:
--------------------------------------------------------------------------------
1 | {
2 | "libraries": [
3 | {
4 | "name": "FoundationFramework"
5 | },
6 | {
7 | "name": "SecretLib"
8 | "path": "SecretLib"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/Foundation.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/Foundation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/Shared/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Helpers.swift
3 | // FoundationFramework
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class GenericHelper {
12 |
13 | public func dummyHelper() {}
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | final class ExampleTest: XCTestCase {
4 |
5 | func test_example_assertion() {
6 | XCTAssertEqual(1,1)
7 | }
8 |
9 | func testAnotherExample() {
10 | mockHelperFunctionInTest()
11 | XCTAssertEqual(2,2)
12 | }
13 |
14 | private func mockHelperFunctionInTest() {
15 |
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFrameworkTests/FoundationFrameworkTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FoundationFrameworkTests.swift
3 | // FoundationFrameworkTests
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import FoundationFramework
11 |
12 | class FoundationFrameworkTests: XCTestCase {
13 |
14 | func testExample() { }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExampleTests/SwiftCodeMetricsExampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftCodeMetricsExampleTests.swift
3 | // SwiftCodeMetricsExampleTests
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftCodeMetricsExample
11 |
12 | class SwiftCodeMetricsExampleTests: XCTestCase {
13 |
14 | func testExample() {}
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/scm_overrides/valid_scm_override.json:
--------------------------------------------------------------------------------
1 | {
2 | "libraries": [
3 | {
4 | "name": "FoundationFramework",
5 | "path": "FoundationFramework",
6 | "is_test": false
7 | },
8 | {
9 | "name": "FoundationFrameworkTests",
10 | "path": "FoundationFrameworkTests",
11 | "is_test": true
12 | },
13 | {
14 | "name": "SecretLib",
15 | "path": "SecretLib",
16 | "is_test": false
17 | }
18 | ],
19 | "shared": [
20 | {
21 | "path": "Shared",
22 | "is_test": false
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | top="$(dirname "$0")"
6 | python3 -m venv "${top}"/venv
7 |
8 | pip_install="${top}/venv/bin/python3 ${top}/venv/bin/pip3 install"
9 |
10 | $pip_install --upgrade "pip>=23.0"
11 |
12 | $pip_install wheel
13 |
14 | if arch | grep -q 'arm64'; then
15 | echo "Overriding pygraphviz setup for M1 architecture"
16 | $pip_install --global-option=build_ext --global-option="-I$(brew --prefix graphviz)/include" --global-option="-L$(brew --prefix graphviz)/lib" pygraphviz
17 | fi
18 |
19 | $pip_install -r requirements.txt
20 |
21 | $pip_install -r tests-requirements.txt
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/scm.json:
--------------------------------------------------------------------------------
1 | {
2 | "libraries": [
3 | {
4 | "name": "FoundationFramework",
5 | "path": "FoundationFramework",
6 | "is_test": false
7 | },
8 | {
9 | "name": "FoundationFrameworkTests",
10 | "path": "FoundationFrameworkTests",
11 | "is_test": true
12 | },
13 | {
14 | "name": "SecretLib",
15 | "path": "SecretLib",
16 | "is_test": false
17 | }
18 | ],
19 | "shared": [
20 | {
21 | "path": "Shared",
22 | "is_test": false
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftCodeMetricsExample
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 |
19 | return true
20 | }
21 |
22 |
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/SecretLib/CaesarChiper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaesarChiper.swift
3 | // SecretLib
4 | //
5 | // Created by Mattia Campolese on 26/02/2019.
6 | // Copyright © 2019 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct CaesarChiper {
12 |
13 | public static func encrypt(message: String, shift: Int) -> String {
14 |
15 | let scalars = Array(message.unicodeScalars)
16 | let unicodePoints = scalars.map({x in Character(UnicodeScalar(Int(x.value) + shift)!)})
17 |
18 | return String(unicodePoints)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogicTests/AwesomeFeatureViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AwesomeFeatureViewControllerTests.swift
3 | // BusinessLogicTests
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import BusinessLogic
11 |
12 | class AwesomeFeatureViewControllerTests: XCTestCase {
13 |
14 | func test_awesomeFeatureViewController_baseRequestPath() {
15 |
16 | XCTAssertEqual(AwesomeFeatureViewController().baseRequestPath, "/such/an/awesome/app")
17 |
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Distribute SCM
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up Python
13 | uses: actions/setup-python@v1
14 | with:
15 | python-version: '3.x'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install setuptools wheel twine
20 | - name: Build and publish
21 | env:
22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
24 | run: |
25 | python setup.py sdist bdist_wheel
26 | twine upload dist/*
27 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFramework/Interfaces/CommonTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommonTypes.swift
3 | // FoundationFramework
4 | //
5 | // Created by Mattia Campolese on 29/09/2020.
6 | // Copyright © 2020 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Result {
12 |
13 | case success(data: Data)
14 | case error(error: Error)
15 |
16 | }
17 |
18 | public enum NetworkingError: Error, Equatable {
19 | case dummyError
20 | }
21 |
22 | public typealias ResultHandler = (Result) -> Void
23 |
24 | public protocol NetworkingProtocol {
25 |
26 | func makeRequest(with dummyUrl: URL, result: ResultHandler)
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFramework/Networking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Networking.swift
3 | // FoundationFramework
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Networking: NetworkingProtocol {
12 |
13 | public init() {}
14 |
15 | public func makeRequest(with dummyUrl: URL, result: ResultHandler) {
16 | if dummyUrl.absoluteString.hasSuffix("error") {
17 | result(.error(error: NetworkingError.dummyError))
18 | } else {
19 | result(.success(data: "Success".data(using: .utf8)!))
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "SCM - Example",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/swift-code-metrics-runner.py",
12 | "console": "integratedTerminal",
13 | "cwd": "${workspaceFolder}",
14 | "args": [
15 | "--source", "swift_code_metrics/tests/test_resources/ExampleProject",
16 | "--artifacts", "demo",
17 | "--generate-graphs"
18 | ]
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogicTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFrameworkTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/SecretLib/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SwiftCodeMetricsExample
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import BusinessLogic
11 |
12 | class ViewController: UIViewController {
13 |
14 | @IBAction func btnLaunchAwesomeAction(_ sender: Any) {
15 | launchFeature(with: true)
16 | }
17 |
18 | @IBAction func btnLaunchNotSoAwesomeAction(_ sender: Any) {
19 | launchFeature(with: false)
20 | }
21 |
22 | private func launchFeature(with awesomeness: Bool) {
23 | let viewController: UIViewController = awesomeness ? AwesomeFeatureViewController() : NotSoAwesomeFeatureViewController()
24 | navigationController?.pushViewController(viewController, animated: true)
25 | }
26 |
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFramework/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mattia Campolese
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | from swift_code_metrics.version import VERSION
3 |
4 | with open("README.md", "r") as fh:
5 | long_description = fh.read()
6 |
7 | install_requires = [
8 | "matplotlib > 2",
9 | "adjustText",
10 | "pygraphviz > 1.5",
11 | "pyfunctional > 1.2",
12 | "numpy > 1.22.0",
13 | "dataclasses"
14 | ]
15 |
16 | setuptools.setup(
17 | name="swift-code-metrics",
18 | version=VERSION,
19 | author="Mattia Campolese",
20 | author_email="matsoftware@gmail.com",
21 | description="Code metrics analyzer for Swift projects.",
22 | long_description=long_description,
23 | long_description_content_type="text/markdown",
24 | url="https://github.com/matsoftware/swift-code-metrics",
25 | packages=setuptools.find_packages(exclude=['contrib', 'docs', 'tests*', 'test']),
26 | entry_points={
27 | "console_scripts": ['swift-code-metrics = swift_code_metrics.scm:main']
28 | },
29 | classifiers=(
30 | "Intended Audience :: Developers",
31 | "Programming Language :: Python :: 3",
32 | "License :: OSI Approved :: MIT License",
33 | "Operating System :: OS Independent",
34 | ),
35 | install_requires=install_requires
36 | )
37 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleFile.swift
3 | //
4 | //
5 | // Created by Mattia Campolese on 06/07/2018.
6 | //
7 |
8 | import Foundation
9 | import AmazingFramework
10 | import struct Helper.CoolFunction
11 | @testable import TestedLibrary
12 |
13 | // First protocol SimpleProtocol
14 | protocol SimpleProtocol {}
15 |
16 | // Unused class protocol
17 | protocol UnusedClassProtocol: class {
18 |
19 | }
20 |
21 | // First class SimpleClass
22 | class SimpleClass: SimpleProtocol {
23 |
24 | func methodOne() {
25 | // Some implementation
26 | }
27 |
28 | func methodTwo(with param1: Int, param2: Int) -> Int {
29 | return param1 + param2
30 | }
31 |
32 | private func privateFunction() {}
33 |
34 | }
35 |
36 | // Second class ComplexClass
37 | final class ComplexClass: SimpleClass {
38 |
39 | /*
40 | This should contain more important code
41 | protocol test
42 | class shouldNotBeRecognized
43 | */
44 |
45 | static func aStaticMethod() {
46 |
47 | }
48 |
49 | }
50 |
51 | // Third element, struct
52 | struct GenericStruct {
53 |
54 | }
55 |
56 | // Fourth class
57 | public final class ComposedAttributedClass {}
58 |
59 | // Fifth class
60 | final fileprivate class ComposedPrivateClass {}
61 |
62 | // Sixth element, struct
63 | internal struct InternalStruct {
64 |
65 | }
66 |
67 | /* yet another comment, this time is in-line - total 20 lines of comments here */
68 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/Foundation/FoundationFrameworkTests/NetworkingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkingTests.swift
3 | // FoundationFrameworkTests
4 | //
5 | // Created by Mattia Campolese on 12/10/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import FoundationFramework
12 |
13 | final class NetworkingTests: XCTestCase {
14 |
15 | var networking: Networking!
16 |
17 | override func setUp() {
18 | super.setUp()
19 | networking = Networking()
20 | }
21 |
22 | override func tearDown() {
23 | networking = nil
24 | super.tearDown()
25 | }
26 |
27 | func test_networking_makeRequest_dummyUrlError_shouldFail() {
28 |
29 | var called = 0
30 | networking.makeRequest(with: URL(string: "http://any.error")!) { res in
31 | called += 1
32 | guard case .error(NetworkingError.dummyError) = res else {
33 | XCTFail("Not an error")
34 | return
35 | }
36 | }
37 |
38 | XCTAssertEqual(called, 1)
39 |
40 | }
41 |
42 | func test_networking_makeRequest_anyOtherUrl_shouldSucceed() {
43 |
44 | var called = 0
45 | networking.makeRequest(with: URL(string: "http://any.other.url")!) { res in
46 | called += 1
47 | guard case .success = res else {
48 | XCTFail("Not a success")
49 | return
50 | }
51 | }
52 |
53 | XCTAssertEqual(called, 1)
54 |
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 | import sys
4 | from swift_code_metrics import scm
5 | from swift_code_metrics._helpers import JSONReader
6 |
7 |
8 | class IntegrationTest(unittest.TestCase):
9 |
10 | def setUp(self):
11 | self.maxDiff = None
12 | sys.argv.clear()
13 | sys.argv.append(os.path.dirname(os.path.realpath(__file__)))
14 | sys.argv.append("--source")
15 | sys.argv.append("swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample")
16 | sys.argv.append("--artifacts")
17 | sys.argv.append("swift_code_metrics/tests/report")
18 | sys.argv.append("--generate-graphs")
19 |
20 | def tearDown(self):
21 | sys.argv.clear()
22 |
23 | def test_sample_app(self):
24 | output_file = "swift_code_metrics/tests/report/output.json"
25 | scm.main() # generate report
26 | expected_file = os.path.join("swift_code_metrics/tests/test_resources", "expected_output.json")
27 | expected_json = sorted(JSONReader.read_json_file(expected_file))
28 | generated_json = sorted(JSONReader.read_json_file(output_file))
29 | assert expected_json == generated_json
30 |
31 |
32 | class IntegrationUnhappyTest(unittest.TestCase):
33 |
34 | def setUp(self):
35 | self.maxDiff = None
36 | sys.argv.clear()
37 | sys.argv.append(os.path.dirname(os.path.realpath(__file__)))
38 | sys.argv.append("--source")
39 | sys.argv.append("any")
40 | sys.argv.append("--artifacts")
41 | sys.argv.append("any")
42 |
43 | def tearDown(self):
44 | sys.argv.clear()
45 |
46 | def test_sample_app(self):
47 | with self.assertRaises(SystemExit) as cm:
48 | scm.main() # should not throw exception and return 0
49 |
50 | self.assertEqual(cm.exception.code, 0)
51 |
52 |
53 | if __name__ == '__main__':
54 | unittest.main()
55 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic/AwesomeFeature.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AwesomeFeature.swift
3 | // BusinessLogic
4 | //
5 | // Created by Mattia Campolese on 05/08/2018.
6 | // Copyright © 2018 Mattia Campolese. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import FoundationFramework
11 | import SecretLib
12 |
13 | public class FeatureViewController: UIViewController {
14 |
15 | var label: UILabel!
16 | var baseRequestPath: String {
17 | assertionFailure("Should override in subclass")
18 | return ""
19 | }
20 |
21 | override public func loadView() {
22 | super.loadView()
23 | label = UILabel()
24 | label.translatesAutoresizingMaskIntoConstraints = false
25 | label.textAlignment = .center
26 | view.addSubview(label)
27 | NSLayoutConstraint.activate([
28 | label.topAnchor.constraint(equalTo: view.topAnchor),
29 | label.bottomAnchor.constraint(equalTo: view.bottomAnchor),
30 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
31 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor),
32 | ])
33 | }
34 |
35 | override public func viewDidLoad() {
36 | super.viewDidLoad()
37 | fetchData()
38 | }
39 |
40 | func fetchData() {
41 | let client = Networking()
42 | client.makeRequest(with: URL(fileURLWithPath: baseRequestPath)) { res in
43 | switch res {
44 | case .success(data: let data):
45 | let plainText = String(data: data, encoding: .utf8)!
46 | label.text = CaesarChiper.encrypt(message: plainText, shift: 9)
47 | view.backgroundColor = .green
48 | case .error(error: let error):
49 | view.backgroundColor = .red
50 | label.text = error.localizedDescription
51 | }
52 | }
53 | }
54 |
55 | }
56 |
57 | public final class AwesomeFeatureViewController: FeatureViewController {
58 |
59 | override var baseRequestPath: String {
60 | return "/such/an/awesome/app"
61 | }
62 |
63 | }
64 |
65 |
66 | public final class NotSoAwesomeFeatureViewController: FeatureViewController {
67 |
68 | override var baseRequestPath: String {
69 | return "/not/an/awesome/error"
70 | }
71 |
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # PyCharm
73 | .idea
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # Environments
88 | .env
89 | .venv
90 | env/
91 | venv/
92 | ENV/
93 | env.bak/
94 | venv.bak/
95 |
96 | # Spyder project settings
97 | .spyderproject
98 | .spyproject
99 |
100 | # Rope project settings
101 | .ropeproject
102 |
103 | # mkdocs documentation
104 | /site
105 |
106 | # mypy
107 | .mypy_cache/
108 |
109 | ## User settings
110 | xcuserdata/
111 |
112 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
113 | *.xcscmblueprint
114 | *.xccheckout
115 |
116 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
117 | build/
118 | DerivedData/
119 | *.moved-aside
120 | *.pbxuser
121 | !default.pbxuser
122 | *.mode1v3
123 | !default.mode1v3
124 | *.mode2v3
125 | !default.mode2v3
126 | *.perspectivev3
127 | !default.perspectivev3
128 |
129 | ### Xcode Patch ###
130 | *.xcodeproj/*
131 | !*.xcodeproj/project.pbxproj
132 | !*.xcodeproj/xcshareddata/
133 | !*.xcworkspace/contents.xcworkspacedata
134 | /*.gcno
135 |
136 | ## Custom
137 |
138 | report
139 | output.json
140 | demo
141 |
--------------------------------------------------------------------------------
/swift_code_metrics/scm.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | from argparse import ArgumentParser
4 |
5 | from ._helpers import Log
6 | from ._analyzer import Inspector
7 | from .version import VERSION
8 | import sys
9 |
10 |
11 | def main():
12 | CLI = ArgumentParser(description='Analyzes the code metrics of a Swift project.')
13 | CLI.add_argument(
14 | '--source',
15 | metavar='S',
16 | nargs='*',
17 | type=str,
18 | default='',
19 | required=True,
20 | help='The root path of the Swift project.'
21 | )
22 | CLI.add_argument(
23 | '--artifacts',
24 | nargs='*',
25 | type=str,
26 | default='',
27 | required=True,
28 | help='Path to save the artifacts generated'
29 | )
30 | CLI.add_argument(
31 | '--exclude',
32 | nargs='*',
33 | type=str,
34 | default=[],
35 | help='List of paths to exclude from analysis (e.g. submodules, pods, checkouts)'
36 | )
37 | CLI.add_argument(
38 | '--tests-paths',
39 | nargs='*',
40 | type=str,
41 | default=['Test', 'Tests'],
42 | help='List of paths that contains test classes and mocks.'
43 | )
44 | CLI.add_argument(
45 | '--generate-graphs',
46 | nargs='?',
47 | type=bool,
48 | const=True,
49 | default=False,
50 | help='Generates the graphic reports and saves them in the artifacts path.'
51 | )
52 | CLI.add_argument(
53 | '--version',
54 | action='version',
55 | version='%(prog)s ' + VERSION
56 | )
57 |
58 | args = CLI.parse_args()
59 | directory = args.source[0]
60 | exclude = args.exclude
61 | artifacts = args.artifacts[0]
62 | default_tests_paths = args.tests_paths
63 | should_generate_graphs = args.generate_graphs
64 |
65 | # Inspects the provided directory
66 | analyzer = Inspector(directory, artifacts, default_tests_paths, exclude)
67 |
68 | if not analyzer.analyze():
69 | Log.warn('No valid swift files found in the project')
70 | sys.exit(0)
71 |
72 | if not should_generate_graphs:
73 | sys.exit(0)
74 |
75 | # Creates graphs
76 | from ._graphs_renderer import GraphsRender
77 | non_test_frameworks = analyzer.filtered_frameworks(is_test=False)
78 | test_frameworks = analyzer.filtered_frameworks(is_test=True)
79 | graphs_renderer = GraphsRender(
80 | artifacts_path=artifacts,
81 | test_frameworks=test_frameworks,
82 | non_test_frameworks=non_test_frameworks,
83 | report=analyzer.report
84 | )
85 | graphs_renderer.render_graphs()
86 |
--------------------------------------------------------------------------------
/swift_code_metrics/_graph_helpers.py:
--------------------------------------------------------------------------------
1 | import string
2 |
3 | import matplotlib.pyplot as plt
4 | import os
5 | from functional import seq
6 | from adjustText import adjust_text
7 | from math import ceil
8 | import pygraphviz as pgv
9 | import numpy as np
10 |
11 |
12 | class Graph:
13 | def __init__(self, path=None):
14 | self.path = path
15 | plt.rc('legend', fontsize='small')
16 |
17 | def bar_plot(self, title, data):
18 | plt.title(title)
19 | plt.ylabel(title)
20 | opacity = 0.8
21 | _ = plt.barh(data[1], data[0], color='blue', alpha=opacity)
22 | index = np.arange(len(data[1]))
23 | plt.yticks(index, data[1], fontsize=5, rotation=30)
24 | self.__render(plt, title)
25 |
26 | def pie_plot(self, title, sizes, labels, legend):
27 | plt.title(title)
28 | patches, _, _ = plt.pie(sizes, labels=labels, autopct='%1.1f%%', shadow=True, startangle=90)
29 | plt.legend(patches, legend, loc='best')
30 | plt.axis('equal')
31 | plt.tight_layout()
32 |
33 | self.__render(plt, title)
34 |
35 | def scattered_plot(self, title, x_label, y_label, data, bands):
36 | plt.title(title)
37 | plt.axis([0, 1, 0, 1])
38 | plt.xlabel(x_label)
39 | plt.ylabel(y_label)
40 |
41 | # Data
42 | x = data[0]
43 | y = data[1]
44 | labels = data[2]
45 |
46 | # Bands
47 | for band in bands:
48 | plt.plot(band[0], band[1])
49 |
50 | # Plot
51 | texts = []
52 | for i, label in enumerate(labels):
53 | plt.plot(x, y, 'ko', label=label)
54 | texts.append(plt.text(x[i], y[i], label, size=8))
55 |
56 | adjust_text(texts, arrowprops=dict(arrowstyle="-", color='k', lw=0.5))
57 |
58 | self.__render(plt, title)
59 |
60 | def directed_graph(self, title, list_of_nodes, list_of_edges):
61 | dir_graph = pgv.AGraph(directed=True, strict=True, rankdir='TD', name=title)
62 | dir_graph.node_attr['shape'] = 'rectangle'
63 |
64 | seq(list_of_nodes).for_each(lambda n: dir_graph.add_node(n[0],
65 | penwidth=ceil((n[1] + 1) / 2), width=(n[1] + 1)))
66 | seq(list_of_edges).for_each(
67 | lambda e: dir_graph.add_edge(e[0], e[1],
68 | label=e[2],
69 | penwidth=ceil((e[3] + 1) / 2),
70 | color=e[4],
71 | fontcolor=e[4]))
72 |
73 | dir_graph.layout('dot')
74 | try:
75 | dir_graph.draw(self.__file_path(title))
76 | except OSError:
77 | # Fallback for minimal graphviz setup
78 | dir_graph.draw(self.__file_path(title, extension='.svg'))
79 |
80 | # Private
81 |
82 | def __render(self, plt, name):
83 | if self.path is None:
84 | plt.show()
85 | else:
86 | save_file = self.__file_path(name)
87 | plt.savefig(save_file, bbox_inches='tight')
88 | plt.close()
89 |
90 | def __file_path(self, name, extension='.pdf'):
91 | filename = Graph.format_filename(name) + extension
92 | return os.path.join(self.path, filename)
93 |
94 | @staticmethod
95 | def format_filename(s):
96 | """Take a string and return a valid filename constructed from the string.
97 | Uses a whitelist approach: any characters not present in valid_chars are
98 | removed. Also spaces are replaced with underscores.
99 |
100 | Note: this method may produce invalid filenames such as ``, `.` or `..`
101 | When I use this method I prepend a date string like '2009_01_15_19_46_32_'
102 | and append a file extension like '.txt', so I avoid the potential of using
103 | an invalid filename.
104 |
105 | https://gist.github.com/seanh/93666
106 |
107 | """
108 | valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
109 | filename = ''.join(c for c in s if c in valid_chars)
110 | filename = filename.replace(' ', '_').lower()
111 | return filename
112 |
--------------------------------------------------------------------------------
/swift_code_metrics/_graphs_renderer.py:
--------------------------------------------------------------------------------
1 | from ._metrics import Metrics, SubModule
2 | from ._report import ReportingHelpers
3 | from dataclasses import dataclass
4 | from typing import List
5 | from ._graphs_presenter import GraphPresenter
6 |
7 |
8 | @dataclass
9 | class GraphsRender:
10 | "Component responsible to generate the needed graphs for the given report."
11 | artifacts_path: str
12 | test_frameworks: List['Framework']
13 | non_test_frameworks: List['Framework']
14 | report: 'Report'
15 |
16 | def render_graphs(self):
17 | graph_presenter = GraphPresenter(self.artifacts_path)
18 |
19 | # Project graphs
20 | self.__project_graphs(graph_presenter=graph_presenter)
21 |
22 | # Submodules graphs
23 | self.__submodules_graphs(graph_presenter=graph_presenter)
24 |
25 | def __project_graphs(self, graph_presenter: 'GraphPresenter'):
26 | # Sorted data plots
27 | non_test_reports_sorted_data = {
28 | 'N. of classes and structs': lambda fr: fr.data.number_of_concrete_data_structures,
29 | 'Lines Of Code - LOC': lambda fr: fr.data.loc,
30 | 'Number Of Comments - NOC': lambda fr: fr.data.noc,
31 | 'N. of imports - NOI': lambda fr: fr.number_of_imports
32 | }
33 |
34 | tests_reports_sorted_data = {
35 | 'Number of tests - NOT': lambda fr: fr.data.number_of_tests
36 | }
37 |
38 | # Non-test graphs
39 | for title, framework_function in non_test_reports_sorted_data.items():
40 | graph_presenter.sorted_data_plot(title, self.non_test_frameworks, framework_function)
41 |
42 | # Distance from the main sequence
43 | all_frameworks = self.test_frameworks + self.non_test_frameworks
44 | graph_presenter.distance_from_main_sequence_plot(self.non_test_frameworks,
45 | lambda fr: Metrics.instability(fr, all_frameworks),
46 | lambda fr: Metrics.abstractness(fr))
47 |
48 | # Dependency graph
49 | graph_presenter.dependency_graph(self.non_test_frameworks,
50 | self.report.non_test_framework_aggregate.loc,
51 | self.report.non_test_framework_aggregate.n_o_i)
52 |
53 | # Code distribution
54 | graph_presenter.frameworks_pie_plot('Code distribution', self.non_test_frameworks,
55 | lambda fr:
56 | ReportingHelpers.decimal_format(fr.data.loc
57 | / self.report.non_test_framework_aggregate.loc))
58 |
59 | # Test graphs
60 | for title, framework_function in tests_reports_sorted_data.items():
61 | graph_presenter.sorted_data_plot(title, self.test_frameworks, framework_function)
62 |
63 | def __submodules_graphs(self, graph_presenter: 'GraphPresenter'):
64 | for framework in self.non_test_frameworks:
65 | GraphsRender.__render_submodules(parent='Code distribution',
66 | root_submodule=framework.submodule,
67 | graph_presenter=graph_presenter)
68 |
69 | @staticmethod
70 | def __render_submodules(parent: str, root_submodule: 'SubModule', graph_presenter: 'GraphPresenter'):
71 | current_submodule = root_submodule.next
72 | while current_submodule != root_submodule:
73 | GraphsRender.__render_submodule_loc(parent=parent,
74 | submodule=current_submodule,
75 | graph_presenter=graph_presenter)
76 | current_submodule = current_submodule.next
77 |
78 | @staticmethod
79 | def __render_submodule_loc(parent: str, submodule: 'SubModule', graph_presenter: 'GraphPresenter'):
80 | submodules = submodule.submodules
81 | if len(submodules) == 0:
82 | return
83 | total_loc = submodule.data.loc
84 | if total_loc == submodules[0].data.loc:
85 | # Single submodule folder - not useful
86 | return
87 | if len(submodule.files) > 0:
88 | # Add a submodule to represent the root slice
89 | submodules = submodules + [SubModule(name='(root)',
90 | files=submodule.files,
91 | submodules=[],
92 | parent=submodule)]
93 |
94 | chart_name = f'{parent} {submodule.path}'
95 | graph_presenter.submodules_pie_plot(chart_name, submodules,
96 | lambda s: ReportingHelpers.decimal_format(s.data.loc / total_loc))
97 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_parser.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from swift_code_metrics._parser import SwiftFileParser, ProjectPathsOverride
3 | from json import JSONDecodeError
4 |
5 |
6 | class ParserTests(unittest.TestCase):
7 |
8 | def __init__(self, *args, **kwargs):
9 | super(ParserTests, self).__init__(*args, **kwargs)
10 | self._generate_mocks()
11 |
12 | def _generate_mocks(self):
13 | self.example_parsed_file = SwiftFileParser(
14 | file="swift_code_metrics/tests/test_resources/ExampleFile.swift",
15 | base_path="swift_code_metrics/tests",
16 | current_subdir="tests",
17 | tests_default_paths=""
18 | ).parse()[0]
19 | self.example_test_file = SwiftFileParser(
20 | file="swift_code_metrics/tests/test_resources/ExampleTest.swift",
21 | base_path="swift_code_metrics/tests",
22 | current_subdir="swift_code_metrics",
23 | tests_default_paths="Test"
24 | ).parse()[0]
25 |
26 | # Non-test file
27 |
28 | def test_swiftparser_parse_should_return_expected_framework_name(self):
29 | self.assertEqual(self.example_parsed_file.framework_name, "test_resources")
30 |
31 | def test_swiftparser_parse_should_return_expected_n_of_comments(self):
32 | self.assertEqual(21, self.example_parsed_file.n_of_comments)
33 |
34 | def test_swiftparser_parse_should_return_expected_loc(self):
35 | self.assertEqual(25, self.example_parsed_file.loc)
36 |
37 | def test_swiftparser_parse_should_return_expected_imports(self):
38 | self.assertEqual(['Foundation', 'AmazingFramework', 'Helper', 'TestedLibrary'], self.example_parsed_file.imports)
39 |
40 | def test_swiftparser_parse_should_return_expected_interfaces(self):
41 | self.assertEqual(['SimpleProtocol', 'UnusedClassProtocol'], self.example_parsed_file.interfaces)
42 |
43 | def test_swiftparser_parse_should_return_expected_structs(self):
44 | self.assertEqual(['GenericStruct', 'InternalStruct'], self.example_parsed_file.structs)
45 |
46 | def test_swiftparser_parse_should_return_expected_classes(self):
47 | self.assertEqual(['SimpleClass',
48 | 'ComplexClass',
49 | 'ComposedAttributedClass',
50 | 'ComposedPrivateClass'], self.example_parsed_file.classes)
51 |
52 | def test_swiftparser_parse_should_return_expected_methods(self):
53 | self.assertEqual(['methodOne',
54 | 'methodTwo',
55 | 'privateFunction',
56 | 'aStaticMethod'], self.example_parsed_file.methods)
57 |
58 | # Test file
59 |
60 | def test_swiftparser_parse_test_file_shouldAppendSuffixFrameworkName(self):
61 | self.assertEqual(self.example_test_file.framework_name, "test_resources_Test")
62 |
63 | def test_swiftfile_noTestClass_numberOfTests_shouldReturnExpectedNumber(self):
64 | self.assertEqual(len(self.example_parsed_file.tests), 0)
65 |
66 | def test_swiftfile_testClass_numberOfTests_shouldReturnExpectedNumber(self):
67 | self.assertEqual(len(self.example_test_file.methods), 3)
68 | self.assertEqual(['test_example_assertion',
69 | 'testAnotherExample'], self.example_test_file.tests)
70 |
71 |
72 | class ProjectPathsOverrideTests(unittest.TestCase):
73 |
74 | def test_projectpathsoverride_loadFromJson_validfile_shouldParseExpectedData(self):
75 | path = "swift_code_metrics/tests/test_resources/scm_overrides/valid_scm_override.json"
76 | path_override = ProjectPathsOverride.load_from_json(path)
77 | expected_path_override = ProjectPathsOverride(entries={
78 | "libraries": [
79 | {
80 | "name": "FoundationFramework",
81 | "path": "FoundationFramework",
82 | "is_test": False
83 | },
84 | {
85 | "name": "FoundationFrameworkTests",
86 | "path": "FoundationFrameworkTests",
87 | "is_test": True
88 | },
89 | {
90 | "name": "SecretLib",
91 | "path": "SecretLib",
92 | "is_test": False
93 | }
94 | ],
95 | "shared": [
96 | {
97 | "path": "Shared",
98 | "is_test": False
99 | }
100 | ]
101 | })
102 | self.assertEqual(path_override, expected_path_override)
103 |
104 | def test_projectpathsoverride_loadFromJson_invalidfile_shouldRaiseException(self):
105 | path = "swift_code_metrics/tests/test_resources/scm_overrides/invalid_scm_override.json"
106 | with self.assertRaises(JSONDecodeError) as cm:
107 | ProjectPathsOverride.load_from_json(path)
108 |
109 | self.assertIsNotNone(cm)
110 |
111 |
112 | if __name__ == '__main__':
113 | unittest.main()
114 |
--------------------------------------------------------------------------------
/swift_code_metrics/_graphs_presenter.py:
--------------------------------------------------------------------------------
1 | from swift_code_metrics._metrics import Metrics
2 | from ._graph_helpers import Graph
3 | from functional import seq
4 | from math import ceil
5 |
6 |
7 | class GraphPresenter:
8 | def __init__(self, artifacts_path):
9 | self.graph = Graph(artifacts_path)
10 |
11 | def sorted_data_plot(self, title, list_of_frameworks, f_of_framework):
12 | """
13 | Renders framework related data to a bar plot.
14 | """
15 | sorted_data = sorted(list(map(lambda f: (f_of_framework(f),
16 | f.name), list_of_frameworks)),
17 | key=lambda tup: tup[0])
18 | plot_data = (list(map(lambda f: f[0], sorted_data)),
19 | list(map(lambda f: f[1], sorted_data)))
20 |
21 | self.graph.bar_plot(title, plot_data)
22 |
23 | def frameworks_pie_plot(self, title, list_of_frameworks, f_of_framework):
24 | """
25 | Renders the percentage distribution data related to a framework in a pie chart.
26 | :param title: The chart title
27 | :param list_of_frameworks: List of frameworks to plot
28 | :param f_of_framework: function on the Framework object
29 | :return:
30 | """
31 | sorted_data = sorted(list(map(lambda f: (f_of_framework(f),
32 | f.compact_name,
33 | f.compact_name_description),
34 | list_of_frameworks)),
35 | key=lambda tup: tup[0])
36 | plot_data = (list(map(lambda f: f[0], sorted_data)),
37 | list(map(lambda f: f[1], sorted_data)),
38 | list(map(lambda f: f[2], sorted_data)),
39 | )
40 |
41 | self.graph.pie_plot(title, plot_data[0], plot_data[1], plot_data[2])
42 |
43 | def submodules_pie_plot(self, title, list_of_submodules, f_of_submodules):
44 | sorted_data = sorted(list(map(lambda s: (f_of_submodules(s),
45 | s.name),
46 | list_of_submodules)),
47 | key=lambda tup: tup[0])
48 | plot_data = (list(map(lambda f: f[0], sorted_data)),
49 | list(map(lambda f: f[1], sorted_data)))
50 |
51 | self.graph.pie_plot(title, plot_data[0], plot_data[1], plot_data[1])
52 |
53 | def distance_from_main_sequence_plot(self, list_of_frameworks, x_ax_f_framework, y_ax_f_framework):
54 | """
55 | Renders framework related data to a scattered plot
56 | """
57 | scattered_data = (list(map(lambda f: x_ax_f_framework(f), list_of_frameworks)),
58 | list(map(lambda f: y_ax_f_framework(f), list_of_frameworks)),
59 | list(map(lambda f: f.name, list_of_frameworks)))
60 |
61 | bands = [
62 | ([1, 0], 'g'),
63 | ([0.66, -0.34], 'y--'),
64 | ([1.34, 0.34], 'y--'),
65 | ([0.34, -0.66], 'r--'),
66 | ([1.66, 0.66], 'r--')
67 | ]
68 |
69 | self.graph.scattered_plot('Deviation from the main sequence',
70 | 'I = Instability',
71 | 'A = Abstractness',
72 | scattered_data,
73 | bands)
74 |
75 | def dependency_graph(self, list_of_frameworks, total_code, total_imports):
76 | """
77 | Renders the Frameworks dependency graph.
78 | """
79 |
80 | nodes = seq(list_of_frameworks).map(lambda fr: (fr.name, ceil(10 * fr.data.loc / total_code))).list()
81 |
82 | internal_edges = seq(list_of_frameworks) \
83 | .flat_map(lambda fr: Metrics.internal_dependencies(fr, list_of_frameworks)) \
84 | .map(lambda ind: GraphPresenter.__make_edge(ind, total_imports, 'forestgreen'))
85 |
86 | external_edges = seq(list_of_frameworks) \
87 | .flat_map(lambda fr: Metrics.external_dependencies(fr, list_of_frameworks)) \
88 | .map(lambda ed: GraphPresenter.__make_edge(ed, total_imports, 'orangered'))
89 |
90 | self.__render_directed_graph('Internal dependencies graph', nodes, internal_edges)
91 | self.__render_directed_graph('External dependencies graph', nodes, external_edges)
92 |
93 | # Total
94 | self.__render_directed_graph('Dependencies graph', nodes, internal_edges + external_edges)
95 |
96 | @staticmethod
97 | def __make_edge(dep, total_imports, color):
98 | return (dep.name, dep.dependent_framework, dep.number_of_imports,
99 | ceil(10 * dep.number_of_imports / total_imports), color)
100 |
101 | def __render_directed_graph(self, title, nodes, edges):
102 | try:
103 | self.graph.directed_graph(title, nodes, edges)
104 | except ValueError:
105 | print('Please ensure that you have Graphviz (https://www.graphviz.org/download) installed.')
106 |
--------------------------------------------------------------------------------
/swift_code_metrics/_report.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 | from ._helpers import ReportingHelpers
3 | from ._metrics import FrameworkData, Framework, Metrics
4 | from ._parser import SwiftFile
5 |
6 |
7 | class ReportProcessor:
8 |
9 | @staticmethod
10 | def generate_report(frameworks: List['Framework'], shared_files: Dict[str, 'SwiftFile']):
11 | report = Report()
12 |
13 | # Shared files
14 | for _, shared_files in shared_files.items():
15 | shared_file = shared_files[0]
16 | shared_file_data = FrameworkData.from_swift_file(swift_file=shared_file)
17 | for _ in range((len(shared_files) - 1)):
18 | report.shared_code += shared_file_data
19 | report.total_aggregate -= shared_file_data
20 | if shared_file.is_test:
21 | report.test_framework_aggregate -= shared_file_data
22 | else:
23 | report.non_test_framework_aggregate -= shared_file_data
24 |
25 | # Frameworks
26 | for f in sorted(frameworks, key=lambda fr: fr.name, reverse=False):
27 | analysis = ReportProcessor.__framework_analysis(f, frameworks)
28 | if f.is_test_framework:
29 | report.tests_framework.append(analysis)
30 | report.test_framework_aggregate.append_framework(f)
31 | else:
32 | report.non_test_framework.append(analysis)
33 | report.non_test_framework_aggregate.append_framework(f)
34 | report.total_aggregate.append_framework(f)
35 |
36 | return report
37 |
38 | @staticmethod
39 | def __framework_analysis(framework: 'Framework', frameworks: List['Framework']) -> Dict:
40 | """
41 | :param framework: The framework to analyze
42 | :return: The architectural analysis of the framework
43 | """
44 | framework_data = framework.data
45 | loc = framework_data.loc
46 | noc = framework_data.noc
47 | poc = Metrics.percentage_of_comments(framework_data.noc,
48 | framework_data.loc)
49 | analysis = Metrics.poc_analysis(poc)
50 | n_a = framework_data.number_of_interfaces
51 | n_c = framework_data.number_of_concrete_data_structures
52 | nom = framework_data.number_of_methods
53 | dependencies = Metrics.total_dependencies(framework)
54 | n_of_tests = framework_data.number_of_tests
55 | n_of_imports = framework.number_of_imports
56 |
57 | # Non-test framework analysis
58 | non_test_analysis = {}
59 | if not framework.is_test_framework:
60 | non_test_analysis["fan_in"] = Metrics.fan_in(framework, frameworks)
61 | non_test_analysis["fan_out"] = Metrics.fan_out(framework)
62 | i = Metrics.instability(framework, frameworks)
63 | a = Metrics.abstractness(framework)
64 | non_test_analysis["i"] = ReportingHelpers.decimal_format(i)
65 | non_test_analysis["a"] = ReportingHelpers.decimal_format(a)
66 | non_test_analysis["d_3"] = ReportingHelpers.decimal_format(
67 | Metrics.distance_main_sequence(framework, frameworks))
68 | analysis += Metrics.ia_analysis(i, a)
69 |
70 | base_analysis = {
71 | "loc": loc,
72 | "noc": noc,
73 | "poc": ReportingHelpers.decimal_format(poc),
74 | "n_a": n_a,
75 | "n_c": n_c,
76 | "nom": nom,
77 | "not": n_of_tests,
78 | "noi": n_of_imports,
79 | "analysis": analysis,
80 | "dependencies": dependencies,
81 | "submodules": framework.submodule.as_dict
82 | }
83 |
84 | return {
85 | framework.name: {**base_analysis, **non_test_analysis}
86 | }
87 |
88 |
89 | class Report:
90 | def __init__(self):
91 | self.non_test_framework = list()
92 | self.tests_framework = list()
93 | self.non_test_framework_aggregate = FrameworkData()
94 | self.test_framework_aggregate = FrameworkData()
95 | self.total_aggregate = FrameworkData()
96 | self.shared_code = FrameworkData()
97 | # Constants for report
98 | self.non_test_frameworks_key = "non-test-frameworks"
99 | self.tests_frameworks_key = "tests-frameworks"
100 | self.aggregate_key = "aggregate"
101 | self.shared_key = "shared"
102 | self.total_key = "total"
103 |
104 | @property
105 | def as_dict(self) -> Dict:
106 | return {
107 | self.non_test_frameworks_key: self.non_test_framework,
108 | self.tests_frameworks_key: self.tests_framework,
109 | self.shared_key: self.shared_code.as_dict,
110 | self.aggregate_key: {
111 | self.non_test_frameworks_key: self.non_test_framework_aggregate.as_dict,
112 | self.tests_frameworks_key: self.test_framework_aggregate.as_dict,
113 | self.total_key: self.total_aggregate.as_dict
114 | }
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fmatsoftware%2Fswift-code-metrics?ref=badge_shield) [](LICENSE) [](https://travis-ci.org/matsoftware/swift-code-metrics) [](https://codecov.io/gh/matsoftware/swift-code-metrics) [](https://www.codacy.com/gh/matsoftware/swift-code-metrics/dashboard?utm_source=github.com&utm_medium=referral&utm_content=matsoftware/swift-code-metrics&utm_campaign=Badge_Grade)
2 | [](https://pypi.python.org/pypi/swift-code-metrics)
3 |
4 | # swift-code-metrics
5 |
6 | Code metrics analyzer for Swift projects.
7 |
8 | |   |
9 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10 | |  |
11 |
12 | ## Introduction
13 |
14 | The goal of this software is to provide an insight of the architectural state of a software written in `Swift` that consists in several modules.
15 | Inspired by the book of Robert C. Martin, _Clean Architecture_, the software will scan the project to identify the different components in order to assess several common code metrics in the software industry:
16 |
17 | - the overall number of concrete classes and interfaces
18 | - the _instability_ and _abstractness_ of the framework
19 | - the _distance from the main sequence_
20 | - LOC (Lines Of Code)
21 | - NOC (Numbers Of Comments)
22 | - POC (Percentage Of Comments)
23 | - NOM (Number of Methods)
24 | - Number of concretes (Number of classes and structs)
25 | - NOT (Number Of Tests)
26 | - NOI (Number Of Imports)
27 | - Frameworks dependency graph (number of internal and external dependencies)
28 |
29 | ## Requirements
30 |
31 | This is a _Python 3_ script that depends on _matplotlib_, _adjustText_, _pyfunctional_ and _pygraphviz_.
32 |
33 | This latest package depends on the [Graphviz](https://www.graphviz.org/download/) binary that must be installed before. If you're in a Mac environment, you can install it directly with `brew install graphviz`.
34 |
35 | ## Usage
36 |
37 | The package is available on `pip` with `pip3 install swift-code-metrics`.
38 |
39 | The syntax is:
40 |
41 | `swift-code-metrics --source --artifacts --exclude --tests-paths --generate-graphs`
42 |
43 | - `--source` is the path to the folder that contains the main Xcode project or Workspace
44 | - `--artifacts` path to the folder that will contain the generated `output.json` report
45 | - `--exclude` (optional) space separated list of path substrings to exclude from analysis (e.g. `Tests` will ignore all files/folders that contain `Tests`)
46 | - `--tests-paths` (default: `Test Tests`) space separated list of path substrings matching test classes
47 | - `--generate-graphs` (optional) if passed, it will generate the graphs related to the analysis and save them in the artifacts folder
48 |
49 | ### Development
50 |
51 | Please run `./install.sh` and `./build_and_test.sh` to install dependencies and run the tests.
52 |
53 | The repo comes with a predefined setup for VS Code to debug and run tests as well.
54 |
55 | ## Documentation
56 |
57 | Please follow the [guide](https://github.com/matsoftware/swift-code-metrics/tree/master/docs/GUIDE.md) with a practical example to get started.
58 |
59 | ## Current limitations
60 |
61 | - This tool is designed for medium/large codebases composed by different frameworks.
62 | The script will scan the directory and it will identify the frameworks by the name of the 'root' folder,
63 | so it's strictly dependent on the file hierarchy (unless a [project path override file](docs/GUIDE.md#Project-paths-override) is specified)
64 |
65 | - Libraries built with `spm` are not supported.
66 |
67 | - The framework name is inferred using the directory structure. If the file is in the root dir, the `default_framework_name` will be used. No inspection of the xcodeproj will be made.
68 |
69 | - The list of methods currently doesn't support computed vars
70 |
71 | - Inline comments in code (such as `struct Data: {} //dummy data`) are currently not supported
72 |
73 | - Only `XCTest` test frameworks are currently supported
74 |
75 | ## TODOs
76 |
77 | - Code improvements
78 | - Other (open to suggestions)
79 |
80 | ## Contact
81 |
82 | [Mattia Campolese](https://www.linkedin.com/in/matcamp/)
83 |
--------------------------------------------------------------------------------
/swift_code_metrics/_analyzer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 |
4 | from ._helpers import AnalyzerHelpers
5 | from ._parser import SwiftFileParser, SwiftFile
6 | from ._metrics import Framework, SubModule
7 | from ._report import ReportProcessor
8 | from functional import seq
9 | from typing import List, Optional
10 |
11 |
12 | class Inspector:
13 | def __init__(self, directory: str, artifacts: str, tests_default_suffixes: List[str], exclude_paths: List[str]):
14 | self.exclude_paths = exclude_paths
15 | self.directory = directory
16 | self.artifacts = artifacts
17 | self.tests_default_suffixes = tests_default_suffixes
18 | self.frameworks = []
19 | self.shared_code = {}
20 | self.report = None
21 |
22 | def analyze(self) -> bool:
23 | if self.directory is not None:
24 | # Initialize report
25 | self.__analyze_directory(self.directory, self.exclude_paths, self.tests_default_suffixes)
26 | if len(self.frameworks) > 0:
27 | self.report = ReportProcessor.generate_report(self.frameworks, self.shared_code)
28 | self._save_report(self.artifacts)
29 | return True
30 | return False
31 |
32 | def filtered_frameworks(self, is_test=False) -> List['Framework']:
33 | return seq(self.frameworks) \
34 | .filter(lambda f: f.is_test_framework == is_test) \
35 | .list()
36 |
37 | def _save_report(self, directory: str):
38 | if not os.path.exists(directory):
39 | os.makedirs(directory)
40 | with open(os.path.join(directory, 'output.json'), 'w') as fp:
41 | json.dump(self.report.as_dict, fp, indent=4)
42 |
43 | # Directory inspection
44 |
45 | def __analyze_directory(self, directory: str, exclude_paths: List[str], tests_default_paths: List[str]):
46 | for subdir, _, files in os.walk(directory):
47 | for file in files:
48 | if file.endswith(AnalyzerHelpers.SWIFT_FILE_EXTENSION) and \
49 | not AnalyzerHelpers.is_path_in_list(subdir, exclude_paths):
50 | full_path = os.path.join(subdir, file)
51 | swift_files = SwiftFileParser(file=full_path,
52 | base_path=directory,
53 | current_subdir=subdir,
54 | tests_default_paths=tests_default_paths).parse()
55 | for swift_file in swift_files:
56 | self.__append_dependency(swift_file)
57 | self.__process_shared_file(swift_file, full_path)
58 |
59 | self.__cleanup_external_dependencies()
60 |
61 | def __append_dependency(self, swift_file: 'SwiftFile'):
62 | framework = self.__get_or_create_framework(swift_file.framework_name)
63 | Inspector.__populate_submodule(framework=framework, swift_file=swift_file)
64 | # This covers the scenario where a test framework might contain no tests
65 | framework.is_test_framework = swift_file.is_test
66 |
67 | for f in swift_file.imports:
68 | imported_framework = self.__get_or_create_framework(f)
69 | if imported_framework is None:
70 | imported_framework = Framework(f)
71 | framework.append_import(imported_framework)
72 |
73 | @staticmethod
74 | def __populate_submodule(framework: 'Framework', swift_file: 'SwiftFile'):
75 | current_paths = str(swift_file.path).split('/')
76 | paths = list(reversed(current_paths))
77 |
78 | submodule = framework.submodule
79 | while len(paths) > 1:
80 | path = paths.pop()
81 | submodules = [s for s in submodule.submodules]
82 | existing_submodule = seq(submodules).filter(lambda sm: sm.name == path)
83 | if len(list(existing_submodule)) > 0:
84 | submodule = existing_submodule.first()
85 | else:
86 | new_submodule = SubModule(name=path, files=[], submodules=[], parent=submodule)
87 | submodule.submodules.append(new_submodule)
88 | submodule = new_submodule
89 |
90 | submodule.files.append(swift_file)
91 |
92 | def __process_shared_file(self, swift_file: 'SwiftFile', directory: str):
93 | if not swift_file.is_shared:
94 | return
95 |
96 | if not self.shared_code.get(directory):
97 | self.shared_code[directory] = [swift_file]
98 | else:
99 | self.shared_code[directory].append(swift_file)
100 |
101 | def __cleanup_external_dependencies(self):
102 | # It will remove external dependencies built as source
103 | self.frameworks = seq(self.frameworks) \
104 | .filter(lambda f: f.number_of_files > 0) \
105 | .list()
106 |
107 | def __get_or_create_framework(self, framework_name: str) -> 'Framework':
108 | framework = self.__get_framework(framework_name)
109 | if framework is None:
110 | # not found, create a new one
111 | framework = Framework(framework_name)
112 | self.frameworks.append(framework)
113 | return framework
114 |
115 | def __get_framework(self, name: str) -> Optional['Framework']:
116 | for f in self.frameworks:
117 | if f.name == name:
118 | return f
119 | return None
120 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [1.5.4](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.5.4) - 2023-08-18
9 |
10 | ### Fixed
11 |
12 | - [PR-45](https://github.com/matsoftware/swift-code-metrics/pull/45) Apple frameworks list update per Xcode 15 beta and CI evergreening
13 |
14 | ## [1.5.3](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.5.3) - 2023-03-17
15 |
16 | ### Fixed
17 |
18 | - [PR-44](https://github.com/matsoftware/swift-code-metrics/pull/44) Relax version requirements for pyfunctional and numpy
19 |
20 | ## [1.5.2](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.5.2) - 2022-01-02
21 |
22 | ### Added
23 |
24 | - Python 3.8 support
25 | - Support for iOS 15 libraries exclusion
26 |
27 | ### Fixed
28 |
29 | - [Issue#33](https://github.com/matsoftware/swift-code-metrics/issues/33) Crash trying to run with generate-graphs
30 | - [Issue#34](https://github.com/matsoftware/swift-code-metrics/issues/34) Improved dependencies resolution
31 | - [Issue#37](https://github.com/matsoftware/swift-code-metrics/issues/37) Support for M1 architecture
32 |
33 | ## [1.5.1](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.5.1) - 2020-02-09
34 |
35 | ### Fixed
36 |
37 | - [PR-32](https://github.com/matsoftware/swift-code-metrics/pull/32) Fix on dependencies version requirements
38 |
39 | ## [1.5.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.5.0) - 2020-10-06
40 |
41 | ### Added
42 |
43 | - [Issue-21](https://github.com/matsoftware/swift-code-metrics/issues/21) Recursive analysis of subdirectories (submodules) in frameworks
44 |
45 | ### Fixed
46 |
47 | - [PR-24](https://github.com/matsoftware/swift-code-metrics/pull/24) Fix on parsing import statements with complex comments
48 |
49 | ## [1.4.1](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.4.1) - 2020-07-29
50 |
51 | ### Added
52 |
53 | - [PR-22](https://github.com/matsoftware/swift-code-metrics/pull/22) Support for iOS 14 / macOS 11 frameworks
54 |
55 | ## [1.4.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.4.0) - 2020-02-25
56 |
57 | ### Added
58 |
59 | - [PR-17](https://github.com/matsoftware/swift-code-metrics/pull/17) Support for iOS 13 / Mac OSX 15 frameworks
60 |
61 | ### Fixed
62 |
63 | - [PR-11](https://github.com/matsoftware/swift-code-metrics/pull/11) Improved layout of bar plots for codebases with many frameworks
64 | - [Issue-12](https://github.com/matsoftware/swift-code-metrics/issues/12) `matplotlib` not initialized if `generate-graphs` is not passed
65 | - [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
66 |
67 | ## [1.3.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.3.0) - 2019-03-14
68 |
69 | ### Added
70 |
71 | - [PR-9](https://github.com/matsoftware/swift-code-metrics/pull/9) Support for multiple frameworks under the same project
72 |
73 | ### Fixed
74 |
75 | - [PR-10](https://github.com/matsoftware/swift-code-metrics/pull/9) Fixed issue when parsing paths with a repeating folder name
76 |
77 | ## [1.2.3](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.2.3) - 2019-02-19
78 |
79 | ### Fixed
80 |
81 | - [PR-6](https://github.com/matsoftware/swift-code-metrics/pull/6)
82 | Gracefully fail on empty projects or code without modules
83 |
84 | ## [1.2.2](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.2.2) - 2019-02-10
85 |
86 | ### Fixed
87 |
88 | - Renamed number of methods acronym (NBM > NOM)
89 |
90 | ## [1.2.1](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.2.1) - 2018-11-07
91 |
92 | ### Fixed
93 |
94 | - Small improvements in graphics legend
95 | - Fix for a redundant message in warnings
96 | - Updated documentation
97 |
98 | ## [1.2.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.2.0) - 2018-11-05
99 |
100 | ### Added
101 |
102 | - Added NOI (number of imports) metric and graph
103 |
104 | ### Changed
105 |
106 | - Improved dependency graphs by using a variable node and arrow thickness
107 | - Improved parsing of import statements
108 | - Improved bar charts' reports
109 |
110 | ### Fixed
111 |
112 | - Analysis of frameworks with no connection with the rest of the code will generate a warning
113 | - Improved code quality and test coverage
114 |
115 | ### Removed
116 |
117 | - Removed A, I and NBM graphs
118 |
119 | ## [1.1.2](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.1.2) - 2018-11-05
120 |
121 | ### Added
122 |
123 | - Added internal and external aggregate dependency graph
124 |
125 | ### Fixed
126 |
127 | - Renamed number of methods acronym (NBM > NOM)
128 | - Removed Apple frameworks from external dependencies
129 |
130 | ## [1.1.1](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.1.1) - 2018-10-23
131 |
132 | ### Added
133 |
134 | - Added list of dependencies in output.json
135 |
136 | ### Fixed
137 |
138 | - Supporting minimal setup of graphviz (fallback on SVG export for the dependency graph)
139 |
140 | ## [1.1.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.1.0) - 2018-10-22
141 |
142 | ### Added
143 |
144 | - Added support to test classes and frameworks with number of tests report and graph
145 | - Added frameworks dependency graph
146 | - Added code distribution chart
147 |
148 | ### Fixed
149 |
150 | - Improved graphs quality
151 | - Updated sample project to Xcode 10 / Swift 4.2
152 | - Extended test coverage
153 | - Updated documentation
154 |
155 | ## [1.0.1](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.0.1) - 2018-09-12
156 |
157 | ### Fixed
158 |
159 | - Enforced UTF-8 encoding on file opening
160 |
161 | ## [1.0.0](https://github.com/matsoftware/swift-code-metrics/releases/tag/1.0.0) - 2018-09-11
162 |
163 | - First stable release
164 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample.xcodeproj/xcshareddata/xcschemes/SwiftCodeMetricsExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
61 |
67 |
68 |
69 |
71 |
77 |
78 |
79 |
81 |
87 |
88 |
89 |
90 |
91 |
101 |
103 |
109 |
110 |
111 |
112 |
118 |
120 |
126 |
127 |
128 |
129 |
131 |
132 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/SwiftCodeMetricsExample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
47 |
58 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/swift_code_metrics/_parser.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from typing import List, Optional, Tuple
4 | from ._helpers import AnalyzerHelpers, ParsingHelpers, JSONReader
5 | from ._helpers import Log
6 |
7 |
8 | class SwiftFile(object):
9 | def __init__(self,
10 | path: str,
11 | framework_name: str,
12 | loc: int,
13 | imports: List[str],
14 | interfaces: List[str],
15 | structs: List[str],
16 | classes: List[str],
17 | methods: List[str],
18 | n_of_comments: int,
19 | is_shared: bool,
20 | is_test: bool):
21 | """
22 | Creates a SwiftFile instance that represents a parsed swift file.
23 | :param path: The path of the file being analyzed
24 | :param framework_name: The framework where the file belongs to.
25 | :param loc: Lines Of Code
26 | :param imports: List of imported frameworks
27 | :param interfaces: List of interfaces (protocols) defined in the file
28 | :param structs: List of structs defined in the file
29 | :param classes: List of classes defined in the file
30 | :param methods: List of functions defined in the file
31 | :param n_of_comments: Total number of comments in the file
32 | :param is_shared: True if the file is shared with other frameworks
33 | :param is_test: True if the file is a test class
34 | """
35 | self.path = path
36 | self.framework_name = framework_name
37 | self.loc = loc
38 | self.imports = imports
39 | self.interfaces = interfaces
40 | self.structs = structs
41 | self.classes = classes
42 | self.methods = methods
43 | self.n_of_comments = n_of_comments
44 | self.is_shared = is_shared
45 | self.is_test = is_test
46 |
47 | @property
48 | def tests(self) -> List[str]:
49 | """
50 | List of test extracted from the parsed methods.
51 | :return: array of strings
52 | """
53 | return list(filter(lambda method: method.startswith(ParsingHelpers.TEST_METHOD_PREFIX),
54 | self.methods))
55 |
56 |
57 | class ProjectPathsOverride(object):
58 |
59 | def __init__(self, **entries):
60 | self.__dict__ = entries['entries']
61 |
62 | def __eq__(self, other):
63 | return (self.libraries == other.libraries) and (self.shared == other.shared)
64 |
65 | @staticmethod
66 | def load_from_json(path: str) -> 'ProjectPathsOverride':
67 | return ProjectPathsOverride(entries=JSONReader.read_json_file(path))
68 |
69 |
70 | class SwiftFileParser(object):
71 | def __init__(self, file: str, base_path: str, current_subdir: str, tests_default_paths: List[str]):
72 | self.file = file
73 | self.base_path = base_path
74 | self.current_subdir = current_subdir
75 | self.tests_default_paths = tests_default_paths
76 | self.imports = []
77 | self.attributes_regex_map = {
78 | ParsingHelpers.IMPORTS: [],
79 | ParsingHelpers.PROTOCOLS: [],
80 | ParsingHelpers.STRUCTS: [],
81 | ParsingHelpers.CLASSES: [],
82 | ParsingHelpers.FUNCS: [],
83 | }
84 |
85 | def parse(self) -> List['SwiftFile']:
86 | """
87 | Parses the .swift file to inspect the code inside.
88 | Notes:
89 | - The framework name is inferred using the directory structure. If the file is in the root dir, the
90 | `default_framework_name` will be used. No inspection of the xcodeproj will be made.
91 | - The list of methods currently doesn't support computed vars
92 | - Inline comments in code (such as `struct Data: {} //dummy data`) are currently not supported
93 | :return: an instance of SwiftFile with the result of the parsing of the provided `file`
94 | """
95 | n_of_comments = 0
96 | loc = 0
97 |
98 | commented_line = False
99 | with open(self.file, encoding='utf-8') as f:
100 | for line in f:
101 | trimmed = line.strip()
102 | if len(trimmed) == 0:
103 | continue
104 |
105 | # Comments
106 | if ParsingHelpers.check_existence(ParsingHelpers.SINGLE_COMMENT, trimmed):
107 | n_of_comments += 1
108 | continue
109 |
110 | if ParsingHelpers.check_existence(ParsingHelpers.BEGIN_COMMENT, trimmed):
111 | commented_line = True
112 | n_of_comments += 1
113 |
114 | if ParsingHelpers.check_existence(ParsingHelpers.END_COMMENT, trimmed):
115 | if not commented_line:
116 | n_of_comments += 1
117 | commented_line = False
118 | continue
119 |
120 | if commented_line:
121 | n_of_comments += 1
122 | continue
123 |
124 | loc += 1
125 |
126 | for key, value in self.attributes_regex_map.items():
127 | extracted_value = ParsingHelpers.extract_substring_with_pattern(key, trimmed)
128 | if len(extracted_value) > 0:
129 | value.append(extracted_value)
130 | continue
131 |
132 | subdir = self.file.replace(self.base_path, '', 1)
133 | first_subpath = self.__extract_first_subpath(subdir)
134 |
135 | framework_names, is_test = self.__extract_overrides(first_subpath) or \
136 | self.__extract_attributes(first_subpath)
137 |
138 | is_shared_file = len(framework_names) > 1
139 | return [SwiftFile(
140 | path=Path(self.current_subdir.replace(f'{self.base_path}/', '')) / Path(self.file).name,
141 | framework_name=f,
142 | loc=loc,
143 | imports=self.attributes_regex_map[ParsingHelpers.IMPORTS],
144 | interfaces=self.attributes_regex_map[ParsingHelpers.PROTOCOLS],
145 | structs=self.attributes_regex_map[ParsingHelpers.STRUCTS],
146 | classes=self.attributes_regex_map[ParsingHelpers.CLASSES],
147 | methods=self.attributes_regex_map[ParsingHelpers.FUNCS],
148 | n_of_comments=n_of_comments,
149 | is_shared=is_shared_file,
150 | is_test=is_test
151 | ) for f in framework_names]
152 |
153 | # Private helpers
154 |
155 | def __extract_overrides(self, first_subpath: str) -> Optional[Tuple[List[str], bool]]:
156 | project_override_path = Path(self.base_path) / first_subpath / ParsingHelpers.FRAMEWORK_STRUCTURE_OVERRIDE_FILE
157 | if not project_override_path.exists():
158 | return None
159 |
160 | # Analysis of custom libraries folder
161 | file_parts = Path(self.file).parts
162 | project_override = ProjectPathsOverride.load_from_json(str(project_override_path))
163 | for library in project_override.libraries:
164 | if library['path'] in file_parts:
165 | return [library['name']], library['is_test']
166 | # Analysis of shared folder
167 | for shared_path in project_override.shared:
168 | if shared_path['path'] in file_parts:
169 | is_test = shared_path['is_test']
170 | libraries = [l['name'] for l in project_override.libraries if l['is_test'] == is_test]
171 | return libraries, shared_path['is_test']
172 |
173 | # No overrides (wrong configuration)
174 | Log.warn(f'{self.file} not classified in a folder with projects overrides (scm.json).')
175 | return None
176 |
177 | def __extract_attributes(self, first_subpath: str) -> Tuple[List[str], bool]:
178 | # Test attribute
179 | is_test = AnalyzerHelpers.is_path_in_list(self.current_subdir, self.tests_default_paths)
180 |
181 | # Root folder files
182 | if first_subpath.endswith(AnalyzerHelpers.SWIFT_FILE_EXTENSION):
183 | return [ParsingHelpers.DEFAULT_FRAMEWORK_NAME], is_test
184 | else:
185 | suffix = ParsingHelpers.DEFAULT_TEST_FRAMEWORK_SUFFIX if is_test else ''
186 | return [first_subpath + suffix], is_test
187 |
188 | def __extract_first_subpath(self, subdir: str) -> str:
189 | subdirs = os.path.split(subdir)
190 | if len(subdirs[0]) > 1:
191 | return self.__extract_first_subpath(subdirs[0])
192 | else:
193 | return subdir.replace('/', '')
194 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_helper.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from swift_code_metrics import _helpers
3 |
4 |
5 | class HelpersTests(unittest.TestCase):
6 |
7 | # Comments
8 |
9 | def test_helpers_begin_comment_check_existence_expectedFalse(self):
10 | regex = _helpers.ParsingHelpers.BEGIN_COMMENT
11 | string = ' Comment /* fake comment */'
12 | self.assertFalse(_helpers.ParsingHelpers.check_existence(regex, string))
13 |
14 | def test_helpers_begin_comment_check_existence_expectedTrue(self):
15 | regex = _helpers.ParsingHelpers.BEGIN_COMMENT
16 | string = '/* True comment */'
17 | self.assertTrue(_helpers.ParsingHelpers.check_existence(regex, string))
18 |
19 | def test_helpers_end_comment_check_existence_expectedFalse(self):
20 | regex = _helpers.ParsingHelpers.END_COMMENT
21 | string = ' Fake comment */ ending'
22 | self.assertFalse(_helpers.ParsingHelpers.check_existence(regex, string))
23 |
24 | def test_helpers_end_comment_check_existence_expectedTrue(self):
25 | regex = _helpers.ParsingHelpers.END_COMMENT
26 | string = ' True comment ending */'
27 | self.assertTrue(_helpers.ParsingHelpers.check_existence(regex, string))
28 |
29 | def test_helpers_single_comment_check_existence_expectedFalse(self):
30 | regex = _helpers.ParsingHelpers.SINGLE_COMMENT
31 | string = 'Fake single // comment //'
32 | self.assertFalse(_helpers.ParsingHelpers.check_existence(regex, string))
33 |
34 | def test_helpers_single_comment_check_existence_expectedTrue(self):
35 | regex = _helpers.ParsingHelpers.SINGLE_COMMENT
36 | string = '// True single line comment'
37 | self.assertTrue(_helpers.ParsingHelpers.check_existence(regex, string))
38 |
39 | # Imports
40 |
41 | def test_helpers_imports_extract_substring_with_pattern_expectFalse(self):
42 | regex = _helpers.ParsingHelpers.IMPORTS
43 | string = '//import Foundation '
44 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
45 |
46 | def test_helpers_imports_leading_semicolon_expectFalse(self):
47 | regex = _helpers.ParsingHelpers.IMPORTS
48 | string = 'import Foundation;'
49 | self.assertEqual('Foundation', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
50 |
51 | def test_helpers_imports_extract_substring_with_pattern_expectTrue(self):
52 | regex = _helpers.ParsingHelpers.IMPORTS
53 | string = 'import Foundation '
54 | self.assertEqual('Foundation', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
55 |
56 | def test_helpers_imports_submodule_expectTrue(self):
57 | regex = _helpers.ParsingHelpers.IMPORTS
58 | string = 'import Foundation.KeyChain'
59 | self.assertEqual('Foundation', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
60 |
61 | def test_helpers_imports_subcomponent_expectTrue(self):
62 | regex = _helpers.ParsingHelpers.IMPORTS
63 | string = 'import struct Foundation.KeyChain'
64 | self.assertEqual('Foundation', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
65 |
66 | def test_helpers_imports_comments_expectTrue(self):
67 | regex = _helpers.ParsingHelpers.IMPORTS
68 | string = 'import Contacts // fix for `dyld: Library not loaded: @rpath/libswiftContacts.dylib`'
69 | self.assertEqual('Contacts', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
70 |
71 | def test_helpers_imports_specialwords_expectFalse(self):
72 | regex = _helpers.ParsingHelpers.IMPORTS
73 | string = 'importedMigratedComponents: AnyImportedComponent'
74 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
75 |
76 | # Protocols
77 |
78 | def test_helpers_protocols_extract_substring_with_pattern_expectFalse(self):
79 | regex = _helpers.ParsingHelpers.PROTOCOLS
80 | string = 'class Conformstoprotocol: Any '
81 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
82 |
83 | def test_helpers_protocols_space_extract_substring_with_pattern_expectTrue(self):
84 | regex = _helpers.ParsingHelpers.PROTOCOLS
85 | string = 'protocol Any : class'
86 | self.assertEqual('Any', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
87 |
88 | def test_helpers_protocols_colons_extract_substring_with_pattern_expectTrue(self):
89 | regex = _helpers.ParsingHelpers.PROTOCOLS
90 | string = 'protocol Any: class'
91 | self.assertEqual('Any', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
92 |
93 | def test_helpers_protocols_property_modifier_extract_substring_with_pattern_expectTrue(self):
94 | regex = _helpers.ParsingHelpers.PROTOCOLS
95 | string = 'public protocol Pubblico{}'
96 | self.assertEqual('Pubblico', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
97 |
98 | # Structs
99 |
100 | def test_helpers_structs_extract_substring_with_pattern_expectFalse(self):
101 | regex = _helpers.ParsingHelpers.STRUCTS
102 | string = 'class Fakestruct: Any '
103 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
104 |
105 | def test_helpers_structs_space_extract_substring_with_pattern_expectTrue(self):
106 | regex = _helpers.ParsingHelpers.STRUCTS
107 | string = 'struct AnyStruct : Protocol {'
108 | self.assertEqual('AnyStruct', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
109 |
110 | def test_helpers_structs_colons_extract_substring_with_pattern_expectTrue(self):
111 | regex = _helpers.ParsingHelpers.STRUCTS
112 | string = 'struct AnyStruct {}'
113 | self.assertEqual('AnyStruct', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
114 |
115 | def test_helpers_structs_property_modifier_extract_substring_with_pattern_expectTrue(self):
116 | regex = _helpers.ParsingHelpers.STRUCTS
117 | string = 'internal struct Internal{'
118 | self.assertEqual('Internal', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
119 |
120 | # Class
121 |
122 | def test_helpers_class_extract_substring_with_pattern_expectFalse(self):
123 | regex = _helpers.ParsingHelpers.CLASSES
124 | string = 'struct Fakeclass: Any '
125 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
126 |
127 | def test_helpers_class_space_extract_substring_with_pattern_expectTrue(self):
128 | regex = _helpers.ParsingHelpers.CLASSES
129 | string = 'class MyClass : Protocol {'
130 | self.assertEqual('MyClass', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
131 |
132 | def test_helpers_class_colons_extract_substring_with_pattern_expectTrue(self):
133 | regex = _helpers.ParsingHelpers.CLASSES
134 | string = 'class MyClass {}'
135 | self.assertEqual('MyClass', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
136 |
137 | def test_helpers_class_property_modifier_extract_substring_with_pattern_expectTrue(self):
138 | regex = _helpers.ParsingHelpers.CLASSES
139 | string = 'private class Private{'
140 | self.assertEqual('Private', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
141 |
142 | # Funcs
143 |
144 | def test_helpers_funcs_extract_substring_with_pattern_expectFalse(self):
145 | regex = _helpers.ParsingHelpers.FUNCS
146 | string = 'struct Fakefunc: Any '
147 | self.assertEqual('', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
148 |
149 | def test_helpers_funcs_space_extract_substring_with_pattern_expectTrue(self):
150 | regex = _helpers.ParsingHelpers.FUNCS
151 | string = 'func myFunction() -> Bool {'
152 | self.assertEqual('myFunction', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
153 |
154 | def test_helpers_funcs_parameters_extract_substring_with_pattern_expectTrue(self):
155 | regex = _helpers.ParsingHelpers.FUNCS
156 | string = 'func myFunction(with parameter: String) '
157 | self.assertEqual('myFunction', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
158 |
159 | def test_helpers_funcs_property_modifier_extract_substring_with_pattern_expectTrue(self):
160 | regex = _helpers.ParsingHelpers.FUNCS
161 | string = 'private func funzione(){'
162 | self.assertEqual('funzione', _helpers.ParsingHelpers.extract_substring_with_pattern(regex, string))
163 |
164 | def test_helpers_funcs_reduce_dictionary(self):
165 | self.assertEqual(3, _helpers.ParsingHelpers.reduce_dictionary({"one": 1, "two": 2}))
166 |
167 |
168 | if __name__ == '__main__':
169 | unittest.main()
170 |
--------------------------------------------------------------------------------
/swift_code_metrics/_helpers.py:
--------------------------------------------------------------------------------
1 | import re
2 | import logging
3 | import json
4 | from typing import Dict
5 | from functional import seq
6 |
7 |
8 | class Log:
9 | __logger = logging.getLogger(__name__)
10 |
11 | @classmethod
12 | def warn(cls, message: str):
13 | Log.__logger.warning(message)
14 |
15 |
16 | class AnalyzerHelpers:
17 | # Constants
18 |
19 | SWIFT_FILE_EXTENSION = '.swift'
20 |
21 | # List of frameworks owned by Apple™️ that are excluded from the analysis.
22 | # This list is manually updated and references the content available from
23 | # https://developer.apple.com/documentation/technologies?changes=latest_major,
24 | # https://developer.apple.com/ios/whats-new/ and https://developer.apple.com/macos/whats-new/
25 | APPLE_FRAMEWORKS = [
26 | 'Accelerate',
27 | 'Accessibility',
28 | 'Accounts',
29 | 'ActivityKit',
30 | 'AddressBook',
31 | 'AddressBookUI',
32 | 'AdServices',
33 | 'AdSupport',
34 | 'AGL',
35 | 'Algorithms',
36 | 'AppClip',
37 | 'AppKit',
38 | 'AppIntents',
39 | 'AppleArchive',
40 | 'AppleTextureEncoder',
41 | 'ApplicationServices',
42 | 'AppTrackingTransparency',
43 | 'ArgumentParser',
44 | 'ARKit',
45 | 'AssetsLibrary',
46 | 'Atomics',
47 | 'AudioToolbox',
48 | 'AudioUnit',
49 | 'AuthenticationServices',
50 | 'AutomatedDeviceEnrollment',
51 | 'AutomaticAssessmentConfiguration',
52 | 'AVFAudio',
53 | 'AVFoundation',
54 | 'AVKit',
55 | 'AVRouting',
56 | 'BackgroundTasks',
57 | 'BusinessChat',
58 | 'CallKit',
59 | 'CarPlay',
60 | 'CFNetwork',
61 | 'Cinematic',
62 | 'ClassKit',
63 | 'ClockKit',
64 | 'CloudKit',
65 | 'Collaboration',
66 | 'ColorSync',
67 | 'Combine',
68 | 'Compression',
69 | 'Contacts',
70 | 'ContactsUI',
71 | 'CoreAnimation',
72 | 'CoreAudio',
73 | 'CoreAudioKit',
74 | 'CoreAudioTypes',
75 | 'CoreBluetooth',
76 | 'CoreData',
77 | 'CoreFoundation',
78 | 'CoreGraphics',
79 | 'CoreHaptics',
80 | 'CoreImage',
81 | 'CoreLocation',
82 | 'CoreLocationUI',
83 | 'CoreMedia',
84 | 'CoreMIDI',
85 | 'CoreML',
86 | 'CoreMotion',
87 | 'CoreNFC',
88 | 'CoreServices',
89 | 'CoreSpotlight',
90 | 'CoreTelephony',
91 | 'CoreTransferable',
92 | 'CoreText',
93 | 'CoreVideo',
94 | 'CoreWLAN',
95 | 'CreateML',
96 | 'CreateMLComponents',
97 | 'CryptoKit',
98 | 'CryptoTokenKit',
99 | 'DarwinNotify',
100 | 'DeveloperToolsSupport',
101 | 'DeviceActivity',
102 | 'DeviceCheck',
103 | 'DeviceDiscoveryExtension',
104 | 'DiskArbitration',
105 | 'Dispatch',
106 | 'Distributed',
107 | 'dnssd',
108 | 'DockKit',
109 | 'DriverKit',
110 | 'EndpointSecurity',
111 | 'EventKit',
112 | 'EventKitUI',
113 | 'ExceptionHandling',
114 | 'ExecutionPolicy',
115 | 'ExposureNotification',
116 | 'ExternalAccessory',
117 | 'ExtensionFoundation',
118 | 'ExtensionKit',
119 | 'FamilyControls',
120 | 'FileProvider',
121 | 'FileProviderUI',
122 | 'FinderSync',
123 | 'ForceFeedback',
124 | 'Foundation',
125 | 'FWAUserLib',
126 | 'FxPlug',
127 | 'GameController',
128 | 'GameKit',
129 | 'GameplayKit',
130 | 'GenerateManual',
131 | 'GLKit',
132 | 'GroupActivities',
133 | 'GSS',
134 | 'HealthKit',
135 | 'HIDDriverKit',
136 | 'HomeKit',
137 | 'HTTPLiveStreaming',
138 | 'Hypervisor',
139 | 'iAd',
140 | 'ImageIO',
141 | 'InputMethodKit',
142 | 'IOBluetooth',
143 | 'IOBluetoothUI',
144 | 'IOKit',
145 | 'IOSurface',
146 | 'IOUSBHost',
147 | 'iTunesLibrary',
148 | 'JavaScriptCore',
149 | 'Kernel',
150 | 'KernelManagement',
151 | 'LatentSemanticMapping',
152 | 'LinkPresentation',
153 | 'LocalAuthentication',
154 | 'Logging',
155 | 'ManagedSettings',
156 | 'ManagedSettingsUI',
157 | 'MapKit',
158 | 'Matter',
159 | 'MatterSupport'
160 | 'MediaAccessibility',
161 | 'MediaLibrary',
162 | 'MediaPlayer',
163 | 'MediaSetup',
164 | 'Messages',
165 | 'MessageUI',
166 | 'Metal',
167 | 'MetalFX',
168 | 'MetalKit',
169 | 'MetalPerformanceShaders',
170 | 'MetalPerformanceShadersGraph',
171 | 'MetricKit',
172 | 'MLCompute',
173 | 'MobileCoreServices',
174 | 'ModelIO',
175 | 'MultipeerConnectivity',
176 | 'MusicKit',
177 | 'NaturalLanguage',
178 | 'NearbyInteraction',
179 | 'Network',
180 | 'NetworkExtension',
181 | 'NetworkingDriverKit',
182 | 'NewsstandKit',
183 | 'NotificationCenter',
184 | 'networkext',
185 | 'ObjectiveC',
186 | 'OpenAL',
187 | 'OpenDirectory',
188 | 'OpenGL',
189 | 'os',
190 | 'os_object',
191 | 'ParavirtualizedGraphics',
192 | 'PassKit',
193 | 'PDFKit',
194 | 'PencilKit',
195 | 'PHASE',
196 | 'PhotoKit',
197 | 'ProximityReader',
198 | 'PushKit',
199 | 'PushToTalk',
200 | 'QTKit',
201 | 'QuartzCore',
202 | 'QuickLook',
203 | 'QuickLookThumbnailing',
204 | 'RealityKit',
205 | 'RegexBuilder',
206 | 'ReplayKit',
207 | 'RoomPlan',
208 | 'SafariServices',
209 | 'SafetyKit',
210 | 'SceneKit',
211 | 'ScreenCaptureKit',
212 | 'ScreenSaver',
213 | 'ScreenTime',
214 | 'Security',
215 | 'SecurityFoundation',
216 | 'SecurityInterface',
217 | 'SensitiveContentAnalysis',
218 | 'SensorKit',
219 | 'ServiceManagement',
220 | 'ShazamKit',
221 | 'simd',
222 | 'SiriKit',
223 | 'SMS',
224 | 'Social',
225 | 'SoundAnalysis',
226 | 'Speech',
227 | 'SpriteKit',
228 | 'StoreKit',
229 | 'StoreKitTest',
230 | 'SwiftData',
231 | 'SwiftUI',
232 | 'Symbols',
233 | 'System',
234 | 'SystemConfiguration',
235 | 'SystemExtensions',
236 | 'TabularData',
237 | 'ThreadNetwork'
238 | 'TVML',
239 | 'TVMLKit JS',
240 | 'TVMLKit',
241 | 'TVServices',
242 | 'TVUIKit',
243 | 'UIKit',
244 | 'UniformTypeIdentifiers',
245 | 'USBDriverKit',
246 | 'UserNotifications',
247 | 'UserNotificationsUI',
248 | 'VideoToolbox',
249 | 'Virtualization',
250 | 'Vision',
251 | 'VisionKit',
252 | 'vmnet'
253 | 'WatchConnectivity',
254 | 'WatchKit',
255 | 'WebKit',
256 | 'WidgetKit',
257 | 'WorkoutKit',
258 | 'XCTest',
259 | 'XPC'
260 | ]
261 |
262 | @staticmethod
263 | def is_path_in_list(subdir, exclude_paths):
264 | for p in exclude_paths:
265 | if p in subdir:
266 | return True
267 | return False
268 |
269 |
270 | class ParsingHelpers:
271 | # Constants
272 |
273 | DEFAULT_FRAMEWORK_NAME = 'AppTarget'
274 | DEFAULT_TEST_FRAMEWORK_SUFFIX = '_Test'
275 | TEST_METHOD_PREFIX = 'test'
276 | FRAMEWORK_STRUCTURE_OVERRIDE_FILE = 'scm.json'
277 |
278 | # Constants - Regex patterns
279 |
280 | BEGIN_COMMENT = r'^//*'
281 | END_COMMENT = r'\*/$'
282 | SINGLE_COMMENT = r'^//'
283 |
284 | IMPORTS = r'(?:(?<=^import )|@testable import )(?:\b\w+\s|)([^.; \/]+)'
285 |
286 | PROTOCOLS = r'.*protocol (.*?)[:|{|\s]'
287 | STRUCTS = r'.*struct (.*?)[:|{|\s]'
288 | CLASSES = r'.*class (.*?)[:|{|\s]'
289 | FUNCS = r'.*func (.*?)[:|\(|\s]'
290 |
291 | # Static helpers
292 |
293 | @staticmethod
294 | def check_existence(regex_pattern, trimmed_string):
295 | regex = re.compile(regex_pattern)
296 | if re.search(regex, trimmed_string.strip()) is not None:
297 | return True
298 | else:
299 | return False
300 |
301 | @staticmethod
302 | def extract_substring_with_pattern(regex_pattern, trimmed_string):
303 | try:
304 | return re.search(regex_pattern, trimmed_string).group(1)
305 | except AttributeError:
306 | return ''
307 |
308 | @staticmethod
309 | def reduce_dictionary(items: Dict[str, int]) -> int:
310 | if len(items.values()) == 0:
311 | return 0
312 | return seq(items.values()) \
313 | .reduce(lambda f1, f2: f1 + f2)
314 |
315 |
316 | class ReportingHelpers:
317 |
318 | @staticmethod
319 | def decimal_format(number, decimal_places=3):
320 | format_string = "{:." + str(decimal_places) + "f}"
321 | return float(format_string.format(number))
322 |
323 |
324 | class JSONReader:
325 |
326 | @staticmethod
327 | def read_json_file(path: str) -> Dict:
328 | with open(path, 'r') as fp:
329 | return json.load(fp)
330 |
--------------------------------------------------------------------------------
/docs/GUIDE.md:
--------------------------------------------------------------------------------
1 | ## Guide
2 |
3 | A sample project is provided in the `resources` folder:
4 |
5 | ```bash
6 | python3 swift-code-metrics-runner.py --source swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample --artifacts report --generate-graphs
7 | ```
8 |
9 | ## Source
10 |
11 | The library will start scanning the content under the `source` folder provided. Frameworks will be identified by their
12 | relative path to the root folder specified, meaning that the file system hierarchy will define the naming convention
13 | (e.g. if a class is under the path `SwiftCodeMetricsExample/BusinessLogic/Logic.swift`, the framework name will be identified
14 | as `BusinessLogic` since `SwiftCodeMetricsExample` is the root of the `source` parameter in the command above).
15 |
16 | ### Submodules
17 |
18 | A Submodule is a folder inside a framework and the library will perform an analysis of the synthetic metrics data for each submodule recursively.
19 | Please make sure that your project folder structure is consistent with your logical groups on Xcode to improve the quality of the analysis
20 | (you can use [synx](https://github.com/venmo/synx) to reconcile groups and local folders).
21 |
22 | ### Project paths override
23 |
24 | Sometimes the folder structure is not enough to provide a description of the libraries involved, for instance when multiple frameworks are
25 | defined inside the same folder and they are reusing shared code. For these scenarios, it's possible to define a `scm.json`
26 | file in the root of the custom folder. This file will let `scm` know how to infer the framework structure.
27 |
28 | Let's consider the following example:
29 |
30 | 
31 |
32 | The `SecretLib` and `FoundationFramework` libraries are both reusing the code in the `Shared` folder and they're both
33 | defined in the same project under the `Foundation` folder. Without an override specification, all of the code inside this
34 | folder will be identifier as part of the `Foundation` framework, which is wrong.
35 |
36 | Let's then define the correct structure by adopting this `scm.json` file:
37 |
38 | ```json
39 | {
40 | "libraries": [
41 | {
42 | "name": "FoundationFramework",
43 | "path": "FoundationFramework",
44 | "is_test": false
45 | },
46 | {
47 | "name": "FoundationFrameworkTests",
48 | "path": "FoundationFrameworkTests",
49 | "is_test": true
50 | },
51 | {
52 | "name": "SecretLib",
53 | "path": "SecretLib",
54 | "is_test": false
55 | }
56 | ],
57 | "shared": [
58 | {
59 | "path": "Shared",
60 | "is_test": false
61 | }
62 | ]
63 | }
64 | ```
65 | The `libraries` array defines the list of frameworks with their relative path.
66 |
67 | The code in the `Shared` folder will contribute to the metrics of every single framework defined in the `libraries`
68 | array but it will be counted only once for the total aggregate data.
69 |
70 | ## Output format
71 |
72 | The `output.json` file will contain the metrics related to all frameworks
73 | and an _aggregate_ result for the project.
74 |
75 | The example below is an excerpt from the example available [here](../swift_code_metrics/tests/test_resources/expected_output.json).
76 |
77 |
78 | Output.json example
79 |
80 | ```json
81 | {
82 | "non-test-frameworks": [
83 | {
84 | "FoundationFramework": {
85 | "loc": 27,
86 | "noc": 21,
87 | "poc": 43.75,
88 | "n_a": 1,
89 | "n_c": 2,
90 | "nom": 3,
91 | "not": 0,
92 | "noi": 0,
93 | "analysis": "The code is over commented. Zone of Pain. Highly stable and concrete component - rigid, hard to extend (not abstract). This component should not be volatile (e.g. a stable foundation library such as Strings).",
94 | "dependencies": [],
95 | "submodules": {
96 | "FoundationFramework": {
97 | "n_of_files": 3,
98 | "metric": {
99 | "loc": 27,
100 | "noc": 21,
101 | "n_a": 1,
102 | "n_c": 2,
103 | "nom": 3,
104 | "not": 0,
105 | "poc": 43.75
106 | },
107 | "submodules": [
108 | {
109 | "Foundation": {
110 | "n_of_files": 3,
111 | "metric": {
112 | "loc": 27,
113 | "noc": 21,
114 | "n_a": 1,
115 | "n_c": 2,
116 | "nom": 3,
117 | "not": 0,
118 | "poc": 43.75
119 | },
120 | "submodules": [
121 | {
122 | "FoundationFramework": {
123 | "n_of_files": 2,
124 | "metric": {
125 | "loc": 23,
126 | "noc": 14,
127 | "n_a": 1,
128 | "n_c": 1,
129 | "nom": 2,
130 | "not": 0,
131 | "poc": 37.838
132 | },
133 | "submodules": [
134 | {
135 | "Interfaces": {
136 | "n_of_files": 1,
137 | "metric": {
138 | "loc": 12,
139 | "noc": 7,
140 | "n_a": 1,
141 | "n_c": 0,
142 | "nom": 1,
143 | "not": 0,
144 | "poc": 36.842
145 | },
146 | "submodules": []
147 | }
148 | }
149 | ]
150 | }
151 | },
152 | {
153 | "Shared": {
154 | "n_of_files": 1,
155 | "metric": {
156 | "loc": 4,
157 | "noc": 7,
158 | "n_a": 0,
159 | "n_c": 1,
160 | "nom": 1,
161 | "not": 0,
162 | "poc": 63.636
163 | },
164 | "submodules": []
165 | }
166 | }
167 | ]
168 | }
169 | }
170 | ]
171 | }
172 | },
173 | "fan_in": 1,
174 | "fan_out": 0,
175 | "i": 0.0,
176 | "a": 0.5,
177 | "d_3": 0.5
178 | }
179 | }
180 | ],
181 | "tests-frameworks": [
182 | {
183 | "BusinessLogic_Test": {
184 | "loc": 7,
185 | "noc": 7,
186 | "poc": 50.0,
187 | "n_a": 0,
188 | "n_c": 1,
189 | "nom": 1,
190 | "not": 1,
191 | "noi": 0,
192 | "analysis": "The code is over commented. ",
193 | "dependencies": []
194 | }
195 | }
196 | ],
197 | "aggregate": {
198 | "non-test-frameworks": {
199 | "loc": 97,
200 | "noc": 35,
201 | "n_a": 1,
202 | "n_c": 7,
203 | "nom": 10,
204 | "not": 0,
205 | "noi": 2,
206 | "poc": 26.515
207 | },
208 | "tests-frameworks": {
209 | "loc": 53,
210 | "noc": 28,
211 | "n_a": 0,
212 | "n_c": 4,
213 | "nom": 7,
214 | "not": 5,
215 | "noi": 0,
216 | "poc": 34.568
217 | },
218 | "total": {
219 | "loc": 150,
220 | "noc": 63,
221 | "n_a": 1,
222 | "n_c": 11,
223 | "nom": 17,
224 | "not": 5,
225 | "noi": 2,
226 | "poc": 29.577
227 | }
228 | }
229 | }
230 | ```
231 |
232 |
233 | KPIs legend:
234 |
235 | | Key | Metric | Description |
236 | |:---------:|:--------------------------------:|:---------------------------------------------------------------------------------------------------:|
237 | | `loc` | Lines Of Code | Number of lines of code (empty lines excluded) |
238 | | `noc` | Number of Comments | Number of comments |
239 | | `poc` | Percentage of Comments | 100 * noc / ( noc + loc) |
240 | | `fan_in` | Fan-In | Incoming dependencies: number of classes outside the framework that depend on classes inside it. |
241 | | `fan_out` | Fan-Out | Outgoing dependencies: number of classes inside this component that depend on classes outside it. |
242 | | `i` | Instability | I = fan_out / (fan_in + fan_out) |
243 | | `n_a` | Number of abstracts | Number of protocols in the framework |
244 | | `n_c` | Number of concretes | Number of struct and classes in the framework |
245 | | `a` | Abstractness | A = n_a / n_c |
246 | | `d_3` | Distance from the main sequence | D³ = abs( A + I - 1 ) |
247 | | `nom` | Number of methods | Number of `func` (computed `var` excluded) |
248 | | `not` | Number of tests | Number of methods in test frameworks starting with `test` |
249 | | `noi` | Number of imports | Number of imported frameworks |
250 |
251 | In addition:
252 |
253 | | Key | Description |
254 | |:--------------:|:------------------------------------------------------------------------------------------:|
255 | | `analysis` | Code metrics analysis on the code regarding percentage of comments and components coupling |
256 | | `dependencies` | List of internal and external dependencies, with number of imports |
257 |
258 |
259 | ## Graphs
260 |
261 | The `--generate-graphs` option will output the following reports:
262 |
263 | ### Components coupling
264 |
265 | #### Dependency graph
266 |
267 | 
268 |
269 | Dependency graph with number of imports of _destination_ from _origin_.
270 |
271 | The framework width and border size are directly proportional to the framework's LOC percentage compared to the total LOC. The thickness of the connection arrow between two frameworks is directly proportional to the percentage of imports call compared to the total number of imports.
272 |
273 | The tool will generate also the _external dependencies graph_ (which will represent the coupling of the source code with external libraries) and the _aggregate dependencies graph_ (which will aggregate both internal and external dependencies).
274 |
275 | #### Distance from main sequence
276 |
277 | 
278 |
279 | It express the components coupling in terms of stability and abstraction.
280 |
281 | Ideally, components should be close to the ideal domain (in green) and the most distant areas are identified as _zones of pain_ (in red).
282 | A framework with I < 0.5 and A < 0.5 indicates a library that's rigid to change, usually a foundation component. Instead, with I > 0.5 and A > 0.5, it's possible to identify components with few dependents that's easy to change, usually representing a container of leftovers or elements still being fully developed.
283 |
284 | For a more detailed description, please refer to the _Clean Architecture, Robert C. Martin_ book, Chapter 14 _Component Coupling_.
285 |
286 | ### Code distribution
287 |
288 |      
289 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_metrics.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from swift_code_metrics._metrics import Framework, Dependency, Metrics, SyntheticData, FrameworkData, SubModule
3 | from swift_code_metrics._parser import SwiftFile
4 | from functional import seq
5 |
6 | example_swiftfile = SwiftFile(
7 | path='/my/path/class.swift',
8 | framework_name='Test',
9 | loc=1,
10 | imports=['Foundation', 'dep1', 'dep2'],
11 | interfaces=['prot1', 'prot2', 'prot3'],
12 | structs=['struct'],
13 | classes=['class'],
14 | methods=['meth1', 'meth2', 'meth3', 'testMethod'],
15 | n_of_comments=7,
16 | is_shared=True,
17 | is_test=False
18 | )
19 |
20 | example_file2 = SwiftFile(
21 | path='/my/path/class.swift',
22 | framework_name='Test',
23 | loc=1,
24 | imports=['Foundation', 'dep1', 'dep2'],
25 | interfaces=['prot1', 'prot2', 'prot3', 'prot4',
26 | 'prot5', 'prot6', 'prot7', 'prot8'],
27 | structs=['struct1', 'struct2'],
28 | classes=['class1', 'class2'],
29 | methods=['meth1', 'meth2', 'meth3', 'testMethod'],
30 | n_of_comments=7,
31 | is_shared=False,
32 | is_test=False
33 | )
34 |
35 |
36 | class FrameworkTests(unittest.TestCase):
37 |
38 | def setUp(self):
39 | self.frameworks = [Framework('BusinessLogic'), Framework('UIKit'), Framework('Other')]
40 | self.framework = Framework('AwesomeName')
41 | self.framework.submodule.files = [example_swiftfile, example_file2]
42 | seq(self.frameworks) \
43 | .for_each(lambda f: self.framework.append_import(f))
44 |
45 | def test_representation(self):
46 | self.assertEqual('AwesomeName(2 files)', str(self.framework))
47 |
48 | def test_compact_name_more_than_four_capitals(self):
49 | test_framework = Framework('FrameworkWithMoreThanFourCapitals')
50 | self.assertEqual('FC', test_framework.compact_name)
51 |
52 | def test_compact_name_less_than_four_capitals(self):
53 | self.assertEqual('AN', self.framework.compact_name)
54 |
55 | def test_compact_name_no_capitals(self):
56 | test_framework = Framework('nocapitals')
57 | self.assertEqual('n', test_framework.compact_name)
58 |
59 | def test_compact_name_description(self):
60 | self.assertEqual('AN = AwesomeName', self.framework.compact_name_description)
61 |
62 | def test_imports(self):
63 | expected_imports = {self.frameworks[0]: 1,
64 | self.frameworks[2]: 1}
65 | self.assertEqual(expected_imports, self.framework.imports)
66 |
67 | def test_number_of_imports(self):
68 | self.assertEqual(2, self.framework.number_of_imports)
69 |
70 | def test_number_of_files(self):
71 | self.assertEqual(2, self.framework.number_of_files)
72 |
73 | def test_synthetic_data(self):
74 | self.assertEqual(self.framework.data.loc, 2)
75 | self.assertEqual(self.framework.data.noc, 14)
76 |
77 |
78 | class DependencyTests(unittest.TestCase):
79 |
80 | def setUp(self):
81 | self.dependency = Dependency('AppLayer', 'DesignKit', 2)
82 |
83 | def test_repr(self):
84 | self.assertEqual('AppLayer - DesignKit (2) imports', str(self.dependency))
85 |
86 | def test_compact_repr(self):
87 | self.assertEqual('AppLayer (2)', self.dependency.compact_repr)
88 |
89 | def test_relation(self):
90 | self.assertEqual('AppLayer > DesignKit', self.dependency.relationship)
91 |
92 |
93 | class MetricsTests(unittest.TestCase):
94 |
95 | def setUp(self):
96 | self._generate_mocks()
97 | self._populate_imports()
98 |
99 | def _generate_mocks(self):
100 | self.foundation_kit = Framework('FoundationKit')
101 | self.design_kit = Framework('DesignKit')
102 | self.app_layer = Framework('ApplicationLayer')
103 | self.rxswift = Framework('RxSwift')
104 | self.test_design_kit = Framework(name='DesignKitTests', is_test_framework=True)
105 | self.awesome_dependency = Framework('AwesomeDependency')
106 | self.not_linked_framework = Framework('External')
107 | self.frameworks = [
108 | self.foundation_kit,
109 | self.design_kit,
110 | self.app_layer,
111 | self.test_design_kit
112 | ]
113 |
114 | def _populate_imports(self):
115 | self.app_layer.append_import(self.design_kit)
116 | self.app_layer.append_import(self.design_kit)
117 | self.app_layer.append_import(self.foundation_kit)
118 | self.test_design_kit.append_import(self.design_kit)
119 |
120 | def test_distance_main_sequence(self):
121 |
122 | example_file = SwiftFile(
123 | path='/my/path/class.swift',
124 | framework_name='Test',
125 | loc=1,
126 | imports=['Foundation', 'dep1', 'dep2'],
127 | interfaces=['prot1', 'prot2'],
128 | structs=['struct1', 'struct2', 'struct3', 'struct4'],
129 | classes=['class1', 'class2', 'class3'],
130 | methods=['meth1', 'meth2', 'meth3', 'testMethod'],
131 | n_of_comments=7,
132 | is_shared=False,
133 | is_test=False
134 | )
135 | self.app_layer.submodule.files.append(example_file)
136 |
137 | self.assertAlmostEqual(0.286,
138 | Metrics.distance_main_sequence(self.app_layer, self.frameworks),
139 | places=3)
140 |
141 | def test_instability_no_imports(self):
142 | self.assertEqual(0, Metrics.instability(self.foundation_kit, self.frameworks))
143 |
144 | def test_instability_not_linked_framework(self):
145 | self.assertEqual(0, Metrics.instability(self.not_linked_framework, self.frameworks))
146 |
147 | def test_instability_imports(self):
148 | self.assertAlmostEqual(1.0, Metrics.instability(self.app_layer, self.frameworks))
149 |
150 | def test_abstractness_no_concretes(self):
151 | self.assertEqual(0, Metrics.abstractness(self.foundation_kit))
152 |
153 | def test_abstractness_concretes(self):
154 | self.foundation_kit.submodule.files.append(example_file2)
155 | self.assertEqual(2, Metrics.abstractness(self.foundation_kit))
156 |
157 | def test_fan_in_test_frameworks(self):
158 | self.assertEqual(2, Metrics.fan_in(self.design_kit, self.frameworks))
159 |
160 | def test_fan_in_no_test_frameworks(self):
161 | self.assertEqual(1, Metrics.fan_in(self.foundation_kit, self.frameworks))
162 |
163 | def test_fan_out(self):
164 | self.assertEqual(3, Metrics.fan_out(self.app_layer))
165 |
166 | def test_external_dependencies(self):
167 | for sf in self.__dummy_external_frameworks:
168 | self.foundation_kit.append_import(sf)
169 |
170 | foundation_external_deps = Metrics.external_dependencies(self.foundation_kit, self.frameworks)
171 | expected_external_deps = [Dependency('FoundationKit', 'RxSwift', 1),
172 | Dependency('FoundationKit', 'AwesomeDependency', 1)]
173 |
174 | design_external_deps = Metrics.external_dependencies(self.design_kit, self.frameworks)
175 |
176 | self.assertEqual(expected_external_deps, foundation_external_deps)
177 | self.assertEqual(design_external_deps, [])
178 |
179 | def test_internal_dependencies(self):
180 | self.design_kit.append_import(self.foundation_kit)
181 |
182 | expected_foundation_internal_deps = []
183 | expected_design_internal_deps = [Dependency('DesignKit', 'FoundationKit', 1)]
184 | expected_app_layer_internal_deps = [Dependency('ApplicationLayer', 'DesignKit', 2),
185 | Dependency('ApplicationLayer', 'FoundationKit', 1)]
186 |
187 | self.assertEqual(expected_foundation_internal_deps,
188 | Metrics.internal_dependencies(self.foundation_kit, self.frameworks))
189 | self.assertEqual(expected_design_internal_deps,
190 | Metrics.internal_dependencies(self.design_kit, self.frameworks))
191 | self.assertEqual(expected_app_layer_internal_deps,
192 | Metrics.internal_dependencies(self.app_layer, self.frameworks))
193 |
194 | def test_total_dependencies(self):
195 | for sf in self.__dummy_external_frameworks:
196 | self.foundation_kit.append_import(sf)
197 | self.foundation_kit.append_import(self.design_kit)
198 |
199 | expected_deps = ['RxSwift(1)', 'AwesomeDependency(1)', 'DesignKit(1)']
200 |
201 | self.assertEqual(expected_deps,
202 | Metrics.total_dependencies(self.foundation_kit))
203 |
204 | def test_poc_valid_loc_noc(self):
205 | self.assertEqual(50, Metrics.percentage_of_comments(loc=2, noc=2))
206 |
207 | def test_poc_invalid_loc_noc(self):
208 | self.assertEqual(0, Metrics.percentage_of_comments(loc=0, noc=0))
209 |
210 | def test_ia_analysis_zone_of_pain(self):
211 | self.assertTrue("Zone of Pain" in Metrics.ia_analysis(0.4, 0.4))
212 |
213 | def test_ia_analysis_zone_of_uselessness(self):
214 | self.assertTrue("Zone of Uselessness" in Metrics.ia_analysis(0.7, 0.7))
215 |
216 | def test_ia_analysis_highly_stable(self):
217 | self.assertTrue("Highly stable component" in Metrics.ia_analysis(0.1, 0.51))
218 |
219 | def test_ia_analysis_highly_unstable(self):
220 | self.assertTrue("Highly unstable component" in Metrics.ia_analysis(0.81, 0.49))
221 |
222 | def test_ia_analysis_low_abstract(self):
223 | self.assertTrue("Low abstract component" in Metrics.ia_analysis(0.51, 0.1))
224 |
225 | def test_ia_analysis_high_abstract(self):
226 | self.assertTrue("High abstract component" in Metrics.ia_analysis(0.49, 0.81))
227 |
228 | @property
229 | def __dummy_external_frameworks(self):
230 | return [
231 | Framework('Foundation'),
232 | Framework('UIKit'),
233 | self.rxswift,
234 | self.awesome_dependency,
235 | ]
236 |
237 |
238 | class SyntheticDataTests(unittest.TestCase):
239 |
240 | def setUp(self):
241 | self.synthetic_data = SyntheticData.from_swift_file(swift_file=example_swiftfile)
242 |
243 | def test_init_no_swift_file(self):
244 | empty_data = SyntheticData()
245 | self.assertEqual(0, empty_data.loc)
246 | self.assertEqual(0, empty_data.noc)
247 | self.assertEqual(0, empty_data.number_of_concrete_data_structures)
248 | self.assertEqual(0, empty_data.number_of_interfaces)
249 | self.assertEqual(0, empty_data.number_of_methods)
250 | self.assertEqual(0, empty_data.number_of_tests)
251 |
252 | def test_synthetic_init_swiftfile(self):
253 | self.assertEqual(1, self.synthetic_data.loc)
254 | self.assertEqual(7, self.synthetic_data.noc)
255 | self.assertEqual(2, self.synthetic_data.number_of_concrete_data_structures)
256 | self.assertEqual(3, self.synthetic_data.number_of_interfaces)
257 | self.assertEqual(4, self.synthetic_data.number_of_methods)
258 | self.assertEqual(1, self.synthetic_data.number_of_tests)
259 |
260 | def test_add_data(self):
261 | additional_data = SyntheticData.from_swift_file(swift_file=example_swiftfile)
262 | self.synthetic_data += additional_data
263 | self.assertEqual(2, self.synthetic_data.loc)
264 | self.assertEqual(14, self.synthetic_data.noc)
265 | self.assertEqual(4, self.synthetic_data.number_of_concrete_data_structures)
266 | self.assertEqual(6, self.synthetic_data.number_of_interfaces)
267 | self.assertEqual(8, self.synthetic_data.number_of_methods)
268 | self.assertEqual(2, self.synthetic_data.number_of_tests)
269 |
270 | def test_subtract_data(self):
271 | additional_data = SyntheticData.from_swift_file(swift_file=example_swiftfile)
272 | self.synthetic_data -= additional_data
273 | self.assertEqual(0, self.synthetic_data.loc)
274 | self.assertEqual(0, self.synthetic_data.noc)
275 | self.assertEqual(0, self.synthetic_data.number_of_concrete_data_structures)
276 | self.assertEqual(0, self.synthetic_data.number_of_interfaces)
277 | self.assertEqual(0, self.synthetic_data.number_of_methods)
278 | self.assertEqual(0, self.synthetic_data.number_of_tests)
279 |
280 | def test_poc(self):
281 | self.assertAlmostEqual(87.5, self.synthetic_data.poc)
282 |
283 | def test_as_dict(self):
284 | expected_dict = {
285 | "loc": 1,
286 | "noc": 7,
287 | "n_a": 3,
288 | "n_c": 2,
289 | "nom": 4,
290 | "not": 1,
291 | "poc": 87.5
292 | }
293 | self.assertEqual(expected_dict, self.synthetic_data.as_dict)
294 |
295 |
296 | class FrameworkDataTests(unittest.TestCase):
297 |
298 | def setUp(self):
299 | self.framework_data = FrameworkData.from_swift_file(swift_file=example_swiftfile)
300 |
301 | def test_init_swift_file(self):
302 | self.assertEqual(1, self.framework_data.loc)
303 | self.assertEqual(7, self.framework_data.noc)
304 | self.assertEqual(2, self.framework_data.number_of_concrete_data_structures)
305 | self.assertEqual(3, self.framework_data.number_of_interfaces)
306 | self.assertEqual(4, self.framework_data.number_of_methods)
307 | self.assertEqual(1, self.framework_data.number_of_tests)
308 | self.assertEqual(2, self.framework_data.n_o_i)
309 |
310 | def test_append_framework(self):
311 | test_framework = Framework('Test')
312 | test_framework.append_import(Framework('Imported'))
313 | test_framework.submodule.files.append(example_swiftfile)
314 |
315 | self.framework_data.append_framework(test_framework)
316 | self.assertEqual(2, self.framework_data.loc)
317 | self.assertEqual(14, self.framework_data.noc)
318 | self.assertEqual(4, self.framework_data.number_of_concrete_data_structures)
319 | self.assertEqual(6, self.framework_data.number_of_interfaces)
320 | self.assertEqual(8, self.framework_data.number_of_methods)
321 | self.assertEqual(2, self.framework_data.number_of_tests)
322 | self.assertEqual(3, self.framework_data.n_o_i)
323 |
324 | def test_remove_framework_data(self):
325 | framework_additional_data = FrameworkData.from_swift_file(swift_file=example_swiftfile)
326 |
327 | self.framework_data -= framework_additional_data
328 | self.assertEqual(0, self.framework_data.loc)
329 | self.assertEqual(0, self.framework_data.noc)
330 | self.assertEqual(0, self.framework_data.number_of_concrete_data_structures)
331 | self.assertEqual(0, self.framework_data.number_of_interfaces)
332 | self.assertEqual(0, self.framework_data.number_of_methods)
333 | self.assertEqual(0, self.framework_data.number_of_tests)
334 | self.assertEqual(0, self.framework_data.n_o_i)
335 |
336 | def test_as_dict(self):
337 | expected_dict = {
338 | "loc": 1,
339 | "noc": 7,
340 | "n_a": 3,
341 | "n_c": 2,
342 | "nom": 4,
343 | "not": 1,
344 | "poc": 87.5,
345 | "noi": 2
346 | }
347 | self.assertEqual(expected_dict, self.framework_data.as_dict)
348 |
349 |
350 | class SubModuleTests(unittest.TestCase):
351 |
352 | def setUp(self):
353 | self.submodule = SubModule(
354 | name="BusinessModule",
355 | files=[example_swiftfile],
356 | submodules=[],
357 | parent=None
358 | )
359 | self.helper = SubModule(
360 | name="Helper",
361 | files=[example_file2],
362 | submodules=[],
363 | parent=self.submodule
364 | )
365 | self.additional_module = SubModule(
366 | name="AdditionalModule",
367 | files=[example_file2],
368 | submodules=[],
369 | parent=self.submodule
370 | )
371 | self.additional_submodule = SubModule(
372 | name="AdditionalSubModule",
373 | files=[example_file2],
374 | submodules=[],
375 | parent=self.additional_module
376 | )
377 | self.additional_module.submodules.append(self.additional_submodule)
378 | self.submodule.submodules.append(self.helper)
379 |
380 | def test_n_of_files(self):
381 | self.assertEqual(2, self.submodule.n_of_files)
382 |
383 | def test_path(self):
384 | self.submodule.submodules.append(self.additional_module)
385 | self.assertEqual('BusinessModule > AdditionalModule > AdditionalSubModule', self.additional_submodule.path)
386 |
387 | def test_next_only_module(self):
388 | self.additional_submodule.parent = None
389 | self.assertEqual(self.additional_submodule, self.additional_submodule.next)
390 |
391 | def test_next_closed_circle(self):
392 | self.submodule.submodules.append(self.additional_module)
393 | # *
394 | # / \
395 | # H AM
396 | # \
397 | # AS
398 | self.assertEqual(self.helper, self.submodule.next)
399 | self.assertEqual(self.additional_module, self.helper.next)
400 | self.assertEqual(self.additional_submodule, self.additional_module.next)
401 | self.assertEqual(self.submodule, self.additional_submodule.next)
402 |
403 | def test_data(self):
404 | data = SyntheticData(
405 | loc=2,
406 | noc=14,
407 | number_of_interfaces=11,
408 | number_of_concrete_data_structures=6,
409 | number_of_methods=8,
410 | number_of_tests=2
411 | )
412 | self.assertEqual(data, self.submodule.data)
413 |
414 | def test_empty_data(self):
415 | data = SyntheticData(
416 | loc=0,
417 | noc=0,
418 | number_of_interfaces=0,
419 | number_of_concrete_data_structures=0,
420 | number_of_methods=0,
421 | number_of_tests=0
422 | )
423 | self.assertEqual(data, SubModule(name="", files=[], submodules=[], parent=None).data)
424 |
425 | def test_dict_repr(self):
426 | self.assertEqual({
427 | "BusinessModule": {
428 | "n_of_files": 2,
429 | "metric": {
430 | "loc": 2,
431 | "n_a": 11,
432 | "n_c": 6,
433 | "noc": 14,
434 | "nom": 8,
435 | "not": 2,
436 | "poc": 87.5
437 | },
438 | "submodules": [
439 | {
440 | "Helper": {
441 | "n_of_files": 1,
442 | "metric": {
443 | "loc": 1,
444 | "n_a": 8,
445 | "n_c": 4,
446 | "noc": 7,
447 | "nom": 4,
448 | "not": 1,
449 | "poc": 87.5
450 | },
451 | "submodules": []
452 | }
453 | }
454 | ]
455 | }
456 | }, self.submodule.as_dict)
457 |
458 |
459 | if __name__ == '__main__':
460 | unittest.main()
461 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/ExampleProject/SwiftCodeMetricsExample/BusinessLogic/BusinessLogic.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E576966E21176D6600CADE76 /* BusinessLogic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E576966421176D6600CADE76 /* BusinessLogic.framework */; };
11 | E576967321176D6600CADE76 /* AwesomeFeatureViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E576967221176D6600CADE76 /* AwesomeFeatureViewControllerTests.swift */; };
12 | E576967F21176DB400CADE76 /* AwesomeFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = E576967E21176DB400CADE76 /* AwesomeFeature.swift */; };
13 | E576968E21176EBF00CADE76 /* FoundationFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E576968D21176EBF00CADE76 /* FoundationFramework.framework */; };
14 | E5BFCA6B222558CA005321F6 /* SecretLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BFCA6A222558CA005321F6 /* SecretLib.framework */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXContainerItemProxy section */
18 | E576966F21176D6600CADE76 /* PBXContainerItemProxy */ = {
19 | isa = PBXContainerItemProxy;
20 | containerPortal = E576965B21176D6600CADE76 /* Project object */;
21 | proxyType = 1;
22 | remoteGlobalIDString = E576966321176D6600CADE76;
23 | remoteInfo = BusinessLogic;
24 | };
25 | /* End PBXContainerItemProxy section */
26 |
27 | /* Begin PBXFileReference section */
28 | E576966421176D6600CADE76 /* BusinessLogic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BusinessLogic.framework; sourceTree = BUILT_PRODUCTS_DIR; };
29 | E576966821176D6600CADE76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
30 | E576966D21176D6600CADE76 /* BusinessLogicTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessLogicTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
31 | E576967221176D6600CADE76 /* AwesomeFeatureViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwesomeFeatureViewControllerTests.swift; sourceTree = ""; };
32 | E576967421176D6600CADE76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
33 | E576967E21176DB400CADE76 /* AwesomeFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwesomeFeature.swift; sourceTree = ""; };
34 | E576968D21176EBF00CADE76 /* FoundationFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FoundationFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; };
35 | E5BFCA6A222558CA005321F6 /* SecretLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SecretLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
36 | /* End PBXFileReference section */
37 |
38 | /* Begin PBXFrameworksBuildPhase section */
39 | E576966021176D6600CADE76 /* Frameworks */ = {
40 | isa = PBXFrameworksBuildPhase;
41 | buildActionMask = 2147483647;
42 | files = (
43 | E5BFCA6B222558CA005321F6 /* SecretLib.framework in Frameworks */,
44 | E576968E21176EBF00CADE76 /* FoundationFramework.framework in Frameworks */,
45 | );
46 | runOnlyForDeploymentPostprocessing = 0;
47 | };
48 | E576966A21176D6600CADE76 /* Frameworks */ = {
49 | isa = PBXFrameworksBuildPhase;
50 | buildActionMask = 2147483647;
51 | files = (
52 | E576966E21176D6600CADE76 /* BusinessLogic.framework in Frameworks */,
53 | );
54 | runOnlyForDeploymentPostprocessing = 0;
55 | };
56 | /* End PBXFrameworksBuildPhase section */
57 |
58 | /* Begin PBXGroup section */
59 | E576965A21176D6600CADE76 = {
60 | isa = PBXGroup;
61 | children = (
62 | E576966621176D6600CADE76 /* BusinessLogic */,
63 | E576967121176D6600CADE76 /* BusinessLogicTests */,
64 | E576966521176D6600CADE76 /* Products */,
65 | E576968C21176EBF00CADE76 /* Frameworks */,
66 | );
67 | sourceTree = "";
68 | };
69 | E576966521176D6600CADE76 /* Products */ = {
70 | isa = PBXGroup;
71 | children = (
72 | E576966421176D6600CADE76 /* BusinessLogic.framework */,
73 | E576966D21176D6600CADE76 /* BusinessLogicTests.xctest */,
74 | );
75 | name = Products;
76 | sourceTree = "";
77 | };
78 | E576966621176D6600CADE76 /* BusinessLogic */ = {
79 | isa = PBXGroup;
80 | children = (
81 | E576966821176D6600CADE76 /* Info.plist */,
82 | E576967E21176DB400CADE76 /* AwesomeFeature.swift */,
83 | );
84 | path = BusinessLogic;
85 | sourceTree = "";
86 | };
87 | E576967121176D6600CADE76 /* BusinessLogicTests */ = {
88 | isa = PBXGroup;
89 | children = (
90 | E576967221176D6600CADE76 /* AwesomeFeatureViewControllerTests.swift */,
91 | E576967421176D6600CADE76 /* Info.plist */,
92 | );
93 | path = BusinessLogicTests;
94 | sourceTree = "";
95 | };
96 | E576968C21176EBF00CADE76 /* Frameworks */ = {
97 | isa = PBXGroup;
98 | children = (
99 | E5BFCA6A222558CA005321F6 /* SecretLib.framework */,
100 | E576968D21176EBF00CADE76 /* FoundationFramework.framework */,
101 | );
102 | name = Frameworks;
103 | sourceTree = "";
104 | };
105 | /* End PBXGroup section */
106 |
107 | /* Begin PBXHeadersBuildPhase section */
108 | E576966121176D6600CADE76 /* Headers */ = {
109 | isa = PBXHeadersBuildPhase;
110 | buildActionMask = 2147483647;
111 | files = (
112 | );
113 | runOnlyForDeploymentPostprocessing = 0;
114 | };
115 | /* End PBXHeadersBuildPhase section */
116 |
117 | /* Begin PBXNativeTarget section */
118 | E576966321176D6600CADE76 /* BusinessLogic */ = {
119 | isa = PBXNativeTarget;
120 | buildConfigurationList = E576967821176D6600CADE76 /* Build configuration list for PBXNativeTarget "BusinessLogic" */;
121 | buildPhases = (
122 | E576965F21176D6600CADE76 /* Sources */,
123 | E576966021176D6600CADE76 /* Frameworks */,
124 | E576966121176D6600CADE76 /* Headers */,
125 | E576966221176D6600CADE76 /* Resources */,
126 | );
127 | buildRules = (
128 | );
129 | dependencies = (
130 | );
131 | name = BusinessLogic;
132 | productName = BusinessLogic;
133 | productReference = E576966421176D6600CADE76 /* BusinessLogic.framework */;
134 | productType = "com.apple.product-type.framework";
135 | };
136 | E576966C21176D6600CADE76 /* BusinessLogicTests */ = {
137 | isa = PBXNativeTarget;
138 | buildConfigurationList = E576967B21176D6600CADE76 /* Build configuration list for PBXNativeTarget "BusinessLogicTests" */;
139 | buildPhases = (
140 | E576966921176D6600CADE76 /* Sources */,
141 | E576966A21176D6600CADE76 /* Frameworks */,
142 | E576966B21176D6600CADE76 /* Resources */,
143 | );
144 | buildRules = (
145 | );
146 | dependencies = (
147 | E576967021176D6600CADE76 /* PBXTargetDependency */,
148 | );
149 | name = BusinessLogicTests;
150 | productName = BusinessLogicTests;
151 | productReference = E576966D21176D6600CADE76 /* BusinessLogicTests.xctest */;
152 | productType = "com.apple.product-type.bundle.unit-test";
153 | };
154 | /* End PBXNativeTarget section */
155 |
156 | /* Begin PBXProject section */
157 | E576965B21176D6600CADE76 /* Project object */ = {
158 | isa = PBXProject;
159 | attributes = {
160 | LastSwiftUpdateCheck = 0940;
161 | LastUpgradeCheck = 1200;
162 | ORGANIZATIONNAME = "Mattia Campolese";
163 | TargetAttributes = {
164 | E576966321176D6600CADE76 = {
165 | CreatedOnToolsVersion = 9.4;
166 | LastSwiftMigration = 1130;
167 | };
168 | E576966C21176D6600CADE76 = {
169 | CreatedOnToolsVersion = 9.4;
170 | LastSwiftMigration = 1130;
171 | };
172 | };
173 | };
174 | buildConfigurationList = E576965E21176D6600CADE76 /* Build configuration list for PBXProject "BusinessLogic" */;
175 | compatibilityVersion = "Xcode 9.3";
176 | developmentRegion = en;
177 | hasScannedForEncodings = 0;
178 | knownRegions = (
179 | en,
180 | Base,
181 | );
182 | mainGroup = E576965A21176D6600CADE76;
183 | productRefGroup = E576966521176D6600CADE76 /* Products */;
184 | projectDirPath = "";
185 | projectRoot = "";
186 | targets = (
187 | E576966321176D6600CADE76 /* BusinessLogic */,
188 | E576966C21176D6600CADE76 /* BusinessLogicTests */,
189 | );
190 | };
191 | /* End PBXProject section */
192 |
193 | /* Begin PBXResourcesBuildPhase section */
194 | E576966221176D6600CADE76 /* Resources */ = {
195 | isa = PBXResourcesBuildPhase;
196 | buildActionMask = 2147483647;
197 | files = (
198 | );
199 | runOnlyForDeploymentPostprocessing = 0;
200 | };
201 | E576966B21176D6600CADE76 /* Resources */ = {
202 | isa = PBXResourcesBuildPhase;
203 | buildActionMask = 2147483647;
204 | files = (
205 | );
206 | runOnlyForDeploymentPostprocessing = 0;
207 | };
208 | /* End PBXResourcesBuildPhase section */
209 |
210 | /* Begin PBXSourcesBuildPhase section */
211 | E576965F21176D6600CADE76 /* Sources */ = {
212 | isa = PBXSourcesBuildPhase;
213 | buildActionMask = 2147483647;
214 | files = (
215 | E576967F21176DB400CADE76 /* AwesomeFeature.swift in Sources */,
216 | );
217 | runOnlyForDeploymentPostprocessing = 0;
218 | };
219 | E576966921176D6600CADE76 /* Sources */ = {
220 | isa = PBXSourcesBuildPhase;
221 | buildActionMask = 2147483647;
222 | files = (
223 | E576967321176D6600CADE76 /* AwesomeFeatureViewControllerTests.swift in Sources */,
224 | );
225 | runOnlyForDeploymentPostprocessing = 0;
226 | };
227 | /* End PBXSourcesBuildPhase section */
228 |
229 | /* Begin PBXTargetDependency section */
230 | E576967021176D6600CADE76 /* PBXTargetDependency */ = {
231 | isa = PBXTargetDependency;
232 | target = E576966321176D6600CADE76 /* BusinessLogic */;
233 | targetProxy = E576966F21176D6600CADE76 /* PBXContainerItemProxy */;
234 | };
235 | /* End PBXTargetDependency section */
236 |
237 | /* Begin XCBuildConfiguration section */
238 | E576967621176D6600CADE76 /* Debug */ = {
239 | isa = XCBuildConfiguration;
240 | buildSettings = {
241 | ALWAYS_SEARCH_USER_PATHS = NO;
242 | CLANG_ANALYZER_NONNULL = YES;
243 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
244 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
245 | CLANG_CXX_LIBRARY = "libc++";
246 | CLANG_ENABLE_MODULES = YES;
247 | CLANG_ENABLE_OBJC_ARC = YES;
248 | CLANG_ENABLE_OBJC_WEAK = YES;
249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
250 | CLANG_WARN_BOOL_CONVERSION = YES;
251 | CLANG_WARN_COMMA = YES;
252 | CLANG_WARN_CONSTANT_CONVERSION = YES;
253 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
255 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
256 | CLANG_WARN_EMPTY_BODY = YES;
257 | CLANG_WARN_ENUM_CONVERSION = YES;
258 | CLANG_WARN_INFINITE_RECURSION = YES;
259 | CLANG_WARN_INT_CONVERSION = YES;
260 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
261 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
262 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
263 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
264 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
266 | CLANG_WARN_STRICT_PROTOTYPES = YES;
267 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
269 | CLANG_WARN_UNREACHABLE_CODE = YES;
270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
271 | CODE_SIGN_IDENTITY = "iPhone Developer";
272 | COPY_PHASE_STRIP = NO;
273 | CURRENT_PROJECT_VERSION = 1;
274 | DEBUG_INFORMATION_FORMAT = dwarf;
275 | ENABLE_STRICT_OBJC_MSGSEND = YES;
276 | ENABLE_TESTABILITY = YES;
277 | GCC_C_LANGUAGE_STANDARD = gnu11;
278 | GCC_DYNAMIC_NO_PIC = NO;
279 | GCC_NO_COMMON_BLOCKS = YES;
280 | GCC_OPTIMIZATION_LEVEL = 0;
281 | GCC_PREPROCESSOR_DEFINITIONS = (
282 | "DEBUG=1",
283 | "$(inherited)",
284 | );
285 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
286 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
287 | GCC_WARN_UNDECLARED_SELECTOR = YES;
288 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
289 | GCC_WARN_UNUSED_FUNCTION = YES;
290 | GCC_WARN_UNUSED_VARIABLE = YES;
291 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
292 | MTL_ENABLE_DEBUG_INFO = YES;
293 | ONLY_ACTIVE_ARCH = YES;
294 | SDKROOT = iphoneos;
295 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
296 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
297 | VERSIONING_SYSTEM = "apple-generic";
298 | VERSION_INFO_PREFIX = "";
299 | };
300 | name = Debug;
301 | };
302 | E576967721176D6600CADE76 /* Release */ = {
303 | isa = XCBuildConfiguration;
304 | buildSettings = {
305 | ALWAYS_SEARCH_USER_PATHS = NO;
306 | CLANG_ANALYZER_NONNULL = YES;
307 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
309 | CLANG_CXX_LIBRARY = "libc++";
310 | CLANG_ENABLE_MODULES = YES;
311 | CLANG_ENABLE_OBJC_ARC = YES;
312 | CLANG_ENABLE_OBJC_WEAK = YES;
313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
314 | CLANG_WARN_BOOL_CONVERSION = YES;
315 | CLANG_WARN_COMMA = YES;
316 | CLANG_WARN_CONSTANT_CONVERSION = YES;
317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
319 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
320 | CLANG_WARN_EMPTY_BODY = YES;
321 | CLANG_WARN_ENUM_CONVERSION = YES;
322 | CLANG_WARN_INFINITE_RECURSION = YES;
323 | CLANG_WARN_INT_CONVERSION = YES;
324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
328 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
329 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
330 | CLANG_WARN_STRICT_PROTOTYPES = YES;
331 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
332 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
333 | CLANG_WARN_UNREACHABLE_CODE = YES;
334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
335 | CODE_SIGN_IDENTITY = "iPhone Developer";
336 | COPY_PHASE_STRIP = NO;
337 | CURRENT_PROJECT_VERSION = 1;
338 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
339 | ENABLE_NS_ASSERTIONS = NO;
340 | ENABLE_STRICT_OBJC_MSGSEND = YES;
341 | GCC_C_LANGUAGE_STANDARD = gnu11;
342 | GCC_NO_COMMON_BLOCKS = YES;
343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
345 | GCC_WARN_UNDECLARED_SELECTOR = YES;
346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
347 | GCC_WARN_UNUSED_FUNCTION = YES;
348 | GCC_WARN_UNUSED_VARIABLE = YES;
349 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
350 | MTL_ENABLE_DEBUG_INFO = NO;
351 | SDKROOT = iphoneos;
352 | SWIFT_COMPILATION_MODE = wholemodule;
353 | SWIFT_OPTIMIZATION_LEVEL = "-O";
354 | VALIDATE_PRODUCT = YES;
355 | VERSIONING_SYSTEM = "apple-generic";
356 | VERSION_INFO_PREFIX = "";
357 | };
358 | name = Release;
359 | };
360 | E576967921176D6600CADE76 /* Debug */ = {
361 | isa = XCBuildConfiguration;
362 | buildSettings = {
363 | CLANG_ENABLE_MODULES = YES;
364 | CODE_SIGN_IDENTITY = "";
365 | CODE_SIGN_STYLE = Automatic;
366 | DEFINES_MODULE = YES;
367 | DYLIB_COMPATIBILITY_VERSION = 1;
368 | DYLIB_CURRENT_VERSION = 1;
369 | DYLIB_INSTALL_NAME_BASE = "@rpath";
370 | INFOPLIST_FILE = BusinessLogic/Info.plist;
371 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
372 | LD_RUNPATH_SEARCH_PATHS = (
373 | "$(inherited)",
374 | "@executable_path/Frameworks",
375 | "@loader_path/Frameworks",
376 | );
377 | PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogic;
378 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
379 | SKIP_INSTALL = YES;
380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
381 | SWIFT_VERSION = 5.0;
382 | TARGETED_DEVICE_FAMILY = "1,2";
383 | };
384 | name = Debug;
385 | };
386 | E576967A21176D6600CADE76 /* Release */ = {
387 | isa = XCBuildConfiguration;
388 | buildSettings = {
389 | CLANG_ENABLE_MODULES = YES;
390 | CODE_SIGN_IDENTITY = "";
391 | CODE_SIGN_STYLE = Automatic;
392 | DEFINES_MODULE = YES;
393 | DYLIB_COMPATIBILITY_VERSION = 1;
394 | DYLIB_CURRENT_VERSION = 1;
395 | DYLIB_INSTALL_NAME_BASE = "@rpath";
396 | INFOPLIST_FILE = BusinessLogic/Info.plist;
397 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
398 | LD_RUNPATH_SEARCH_PATHS = (
399 | "$(inherited)",
400 | "@executable_path/Frameworks",
401 | "@loader_path/Frameworks",
402 | );
403 | PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogic;
404 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
405 | SKIP_INSTALL = YES;
406 | SWIFT_VERSION = 5.0;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | };
409 | name = Release;
410 | };
411 | E576967C21176D6600CADE76 /* Debug */ = {
412 | isa = XCBuildConfiguration;
413 | buildSettings = {
414 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
415 | CODE_SIGN_STYLE = Automatic;
416 | INFOPLIST_FILE = BusinessLogicTests/Info.plist;
417 | LD_RUNPATH_SEARCH_PATHS = (
418 | "$(inherited)",
419 | "@executable_path/Frameworks",
420 | "@loader_path/Frameworks",
421 | );
422 | PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogicTests;
423 | PRODUCT_NAME = "$(TARGET_NAME)";
424 | SWIFT_VERSION = 5.0;
425 | TARGETED_DEVICE_FAMILY = "1,2";
426 | };
427 | name = Debug;
428 | };
429 | E576967D21176D6600CADE76 /* Release */ = {
430 | isa = XCBuildConfiguration;
431 | buildSettings = {
432 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
433 | CODE_SIGN_STYLE = Automatic;
434 | INFOPLIST_FILE = BusinessLogicTests/Info.plist;
435 | LD_RUNPATH_SEARCH_PATHS = (
436 | "$(inherited)",
437 | "@executable_path/Frameworks",
438 | "@loader_path/Frameworks",
439 | );
440 | PRODUCT_BUNDLE_IDENTIFIER = uk.easyfutureltd.BusinessLogicTests;
441 | PRODUCT_NAME = "$(TARGET_NAME)";
442 | SWIFT_VERSION = 5.0;
443 | TARGETED_DEVICE_FAMILY = "1,2";
444 | };
445 | name = Release;
446 | };
447 | /* End XCBuildConfiguration section */
448 |
449 | /* Begin XCConfigurationList section */
450 | E576965E21176D6600CADE76 /* Build configuration list for PBXProject "BusinessLogic" */ = {
451 | isa = XCConfigurationList;
452 | buildConfigurations = (
453 | E576967621176D6600CADE76 /* Debug */,
454 | E576967721176D6600CADE76 /* Release */,
455 | );
456 | defaultConfigurationIsVisible = 0;
457 | defaultConfigurationName = Release;
458 | };
459 | E576967821176D6600CADE76 /* Build configuration list for PBXNativeTarget "BusinessLogic" */ = {
460 | isa = XCConfigurationList;
461 | buildConfigurations = (
462 | E576967921176D6600CADE76 /* Debug */,
463 | E576967A21176D6600CADE76 /* Release */,
464 | );
465 | defaultConfigurationIsVisible = 0;
466 | defaultConfigurationName = Release;
467 | };
468 | E576967B21176D6600CADE76 /* Build configuration list for PBXNativeTarget "BusinessLogicTests" */ = {
469 | isa = XCConfigurationList;
470 | buildConfigurations = (
471 | E576967C21176D6600CADE76 /* Debug */,
472 | E576967D21176D6600CADE76 /* Release */,
473 | );
474 | defaultConfigurationIsVisible = 0;
475 | defaultConfigurationName = Release;
476 | };
477 | /* End XCConfigurationList section */
478 | };
479 | rootObject = E576965B21176D6600CADE76 /* Project object */;
480 | }
481 |
--------------------------------------------------------------------------------
/swift_code_metrics/_metrics.py:
--------------------------------------------------------------------------------
1 | from ._helpers import AnalyzerHelpers, Log, ParsingHelpers, ReportingHelpers
2 | from ._parser import SwiftFile
3 | from dataclasses import dataclass
4 | from functional import seq
5 | from typing import Dict, List, Optional
6 |
7 |
8 | class Metrics:
9 |
10 | @staticmethod
11 | def distance_main_sequence(framework: 'Framework', frameworks: List['Framework']) -> float:
12 | """
13 | Distance from the main sequence (sweet spot in the A/I ratio)
14 | D³ = |A+I-1|
15 | D = 0: the component is on the Main Sequence (optimal)
16 | D = 1: the component is at the maximum distance from the main sequence (worst case)
17 | :param framework: The framework to analyze
18 | :param frameworks: The other frameworks in the project
19 | :return: the D³ value (from 0 to 1)
20 | """
21 | return abs(Metrics.abstractness(framework) + Metrics.instability(framework, frameworks) - 1)
22 |
23 | @staticmethod
24 | def instability(framework: 'Framework', frameworks: List['Framework']) -> float:
25 | """
26 | Instability: I = fan-out / (fan-in + fan-out)
27 | I = 0: maximally stable component
28 | I = 1: maximally unstable component
29 | :param framework: The framework to analyze
30 | :param frameworks: The other frameworks in the project
31 | :return: the instability value (float)
32 | """
33 | fan_in = Metrics.fan_in(framework, frameworks)
34 | fan_out = Metrics.fan_out(framework)
35 | sum_in_out = fan_in + fan_out
36 | if sum_in_out == 0:
37 | Log.warn(f'{framework.name} is not linked with the rest of the project.')
38 | return 0
39 | return fan_out / sum_in_out
40 |
41 | @staticmethod
42 | def abstractness(framework: 'Framework') -> float:
43 | """
44 | A = Na / Nc
45 | A = 0: maximally abstract component
46 | A = 1: maximally concrete component
47 | :param framework: The framework to analyze
48 | :return: The abstractness value (float)
49 | """
50 | if framework.data.number_of_concrete_data_structures == 0:
51 | Log.warn(f'{framework.name} is an external dependency.')
52 | return 0
53 | else:
54 | return framework.data.number_of_interfaces / framework.data.number_of_concrete_data_structures
55 |
56 | @staticmethod
57 | def fan_in(framework: 'Framework', frameworks: List['Framework']) -> int:
58 | """
59 | Fan-In: incoming dependencies (number of classes outside the framework that depend on classes inside it)
60 | :param framework: The framework to analyze
61 | :param frameworks: The other frameworks in the project
62 | :return: The Fan-In value (int)
63 | """
64 | fan_in = 0
65 | for f in Metrics.__other_nontest_frameworks(framework, frameworks):
66 | existing = f.imports.get(framework, 0)
67 | fan_in += existing
68 | return fan_in
69 |
70 | @staticmethod
71 | def fan_out(framework: 'Framework') -> int:
72 | """
73 | Fan-Out: outgoing dependencies. (number of classes inside this component that depend on classes outside it)
74 | :param framework: The framework to analyze
75 | :return: The Fan-Out value (int)
76 | """
77 | fan_out = 0
78 | for _, value in framework.imports.items():
79 | fan_out += value
80 | return fan_out
81 |
82 | @staticmethod
83 | def external_dependencies(framework: 'Framework', frameworks: List['Framework']) -> List['Dependency']:
84 | """
85 | :param framework: The framework to inspect for imports
86 | :param frameworks: The other frameworks in the project
87 | :return: List of imported frameworks that are external to the project (e.g third party libraries).
88 | System libraries excluded.
89 | """
90 | return Metrics.__filtered_imports(framework, frameworks, is_internal=False)
91 |
92 | @staticmethod
93 | def internal_dependencies(framework: 'Framework', frameworks: List['Framework']) -> List['Dependency']:
94 | """
95 | :param framework: The framework to inspect for imports
96 | :param frameworks: The other frameworks in the project
97 | :return: List of imported frameworks that are internal to the project
98 | """
99 | return Metrics.__filtered_imports(framework, frameworks, is_internal=True)
100 |
101 | @staticmethod
102 | def total_dependencies(framework: 'Framework') -> List[str]:
103 | """
104 | :param framework: The framework to inspect
105 | :return: The list of imported frameworks description
106 | """
107 | return seq(framework.imports.items()) \
108 | .map(lambda f: f'{f[0].name}({str(f[1])})') \
109 | .list()
110 |
111 | @staticmethod
112 | def percentage_of_comments(noc: int, loc: int) -> float:
113 | """
114 | Percentage Of Comments (POC) = 100 * NoC / ( NoC + LoC)
115 | :param noc: The number of lines of comments
116 | :param loc: the number of lines of code
117 | :return: The POC value (double)
118 | """
119 | noc_loc = noc + loc
120 | if noc_loc == 0:
121 | return 0
122 | return 100 * noc / noc_loc
123 |
124 | # Analysis
125 |
126 | @staticmethod
127 | def ia_analysis(instability: float, abstractness: float) -> str:
128 | """
129 | Verbose qualitative analysis of instability and abstractness.
130 | :param instability: The instability value of the framework
131 | :param abstractness: The abstractness value of the framework
132 | :return: Textual analysis.
133 | """
134 | if instability <= 0.5 and abstractness <= 0.5:
135 | return 'Zone of Pain. Highly stable and concrete component - rigid, hard to extend (not abstract). ' \
136 | 'This component should not be volatile (e.g. a stable foundation library such as Strings).'
137 | elif instability >= 0.5 and abstractness >= 0.5:
138 | return 'Zone of Uselessness. Maximally abstract with few or no dependents - potentially useless. ' \
139 | 'This component is high likely a leftover that should be removed.'
140 |
141 | # Standard components
142 |
143 | res = ''
144 |
145 | # I analysis
146 | if instability < 0.2:
147 | res += 'Highly stable component (hard to change, responsible and independent). '
148 | elif instability > 0.8:
149 | res += 'Highly unstable component (lack of dependents, easy to change, irresponsible) '
150 |
151 | # A analysis
152 |
153 | if abstractness < 0.2:
154 | res += 'Low abstract component, few interfaces. '
155 | elif abstractness > 0.8:
156 | res += 'High abstract component, few concrete data structures. '
157 |
158 | return res
159 |
160 | @staticmethod
161 | def poc_analysis(poc: float) -> str:
162 | if poc <= 20:
163 | return 'The code is under commented. '
164 | if poc >= 40:
165 | return 'The code is over commented. '
166 |
167 | return ''
168 |
169 | # Internal
170 |
171 | @staticmethod
172 | def __other_nontest_frameworks(framework: 'Framework', frameworks: List['Framework']) -> List['Framework']:
173 | return seq(frameworks) \
174 | .filter(lambda f: f is not framework and not f.is_test_framework) \
175 | .list()
176 |
177 | @staticmethod
178 | def __filtered_imports(framework: 'Framework',
179 | frameworks: List['Framework'],
180 | is_internal: bool) -> List['Dependency']:
181 | return seq(framework.imports.items()) \
182 | .filter(lambda f: (Metrics.__is_name_contained_in_list(f[0], frameworks)) == is_internal) \
183 | .map(lambda imp: Dependency(name=framework.name,
184 | dependent_framework=imp[0].name,
185 | number_of_imports=imp[1])) \
186 | .list()
187 |
188 | @staticmethod
189 | def __is_name_contained_in_list(framework: 'Framework', frameworks: List['Framework']) -> bool:
190 | return len(seq(frameworks)
191 | .filter(lambda f: f.name == framework.name)
192 | .list()) > 0
193 |
194 |
195 | @dataclass
196 | class SyntheticData:
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 | )
250 |
251 | @property
252 | def poc(self) -> float:
253 | return Metrics.percentage_of_comments(self.noc, self.loc)
254 |
255 | @property
256 | def as_dict(self) -> Dict:
257 | return {
258 | "loc": self.loc,
259 | "noc": self.noc,
260 | "n_a": self.number_of_interfaces,
261 | "n_c": self.number_of_concrete_data_structures,
262 | "nom": self.number_of_methods,
263 | "not": self.number_of_tests,
264 | "poc": ReportingHelpers.decimal_format(self.poc)
265 | }
266 |
267 |
268 | @dataclass()
269 | class FrameworkData(SyntheticData):
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)
300 |
301 | def append_framework(self, f: 'Framework'):
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
309 | self.n_o_i += f.number_of_imports
310 |
311 | @property
312 | def as_dict(self) -> Dict:
313 | return {**super().as_dict, **{"noi": self.n_o_i}}
314 |
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 |
339 |
340 | class Framework:
341 | def __init__(self, name: str, is_test_framework: bool = False):
342 | self.name = name
343 | self.__total_imports = {}
344 | self.submodule = SubModule(
345 | name=self.name,
346 | files=[],
347 | submodules=[],
348 | parent=None
349 | )
350 | self.is_test_framework = is_test_framework
351 |
352 | def __repr__(self):
353 | return self.name + '(' + str(self.number_of_files) + ' files)'
354 |
355 | def append_import(self, framework_import: 'Framework'):
356 | """
357 | Adds the dependent framework to the list of imported dependencies
358 | :param framework_import: The framework that is being imported
359 | :return:
360 | """
361 | existing_framework = self.__total_imports.get(framework_import)
362 | if not existing_framework:
363 | self.__total_imports[framework_import] = 1
364 | else:
365 | self.__total_imports[framework_import] += 1
366 |
367 | @property
368 | def data(self) -> SyntheticData:
369 | """
370 | The metrics data describing the framework
371 | :return: an instance of SyntheticData
372 | """
373 | return self.submodule.data
374 |
375 | @property
376 | def number_of_files(self) -> int:
377 | """
378 | Number of files in the framework
379 | :return: The total number of files in this framework (int)
380 | """
381 | return self.submodule.n_of_files
382 |
383 | @property
384 | def imports(self) -> Dict[str, int]:
385 | """
386 | Returns the list of framework imports without Apple libraries
387 | :return: list of filtered imports
388 | """
389 | return Framework.__filtered_imports(self.__total_imports.items())
390 |
391 | @property
392 | def number_of_imports(self) -> int:
393 | """
394 | :return: The total number of imports for this framework
395 | """
396 | return ParsingHelpers.reduce_dictionary(self.imports)
397 |
398 | @property
399 | def compact_name(self) -> str:
400 | all_capitals = ''.join(c for c in self.name if c.isupper())
401 | if len(all_capitals) > 4:
402 | return all_capitals[0] + all_capitals[-1:]
403 | elif len(all_capitals) == 0:
404 | return self.name[0]
405 | else:
406 | return all_capitals
407 |
408 | @property
409 | def compact_name_description(self) -> str:
410 | return f'{self.compact_name} = {self.name}'
411 |
412 | # Static
413 |
414 | @staticmethod
415 | def __filtered_imports(items: 'ItemsView') -> Dict[str, int]:
416 | return seq(items).filter(lambda f: f[0].name not in AnalyzerHelpers.APPLE_FRAMEWORKS).dict()
417 |
418 |
419 | @dataclass
420 | class SubModule:
421 | """
422 | Representation of a submodule inside a Framework
423 | """
424 | name: str
425 | files: List['SwiftFile']
426 | submodules: List['SubModule']
427 | parent: Optional['SubModule']
428 |
429 | @property
430 | def next(self) -> 'SubModule':
431 | if len(self.submodules) == 0:
432 | if self.parent is None:
433 | return self
434 | else:
435 | return self.submodules[0]
436 |
437 | next_level = self.parent
438 | current_level = self
439 | while next_level is not None:
440 | next_i = next_level.submodules.index(current_level) + 1
441 | if next_i < len(next_level.submodules):
442 | return next_level.submodules[next_i]
443 | else:
444 | current_level = next_level
445 | next_level = next_level.parent
446 |
447 | return current_level
448 |
449 | @property
450 | def n_of_files(self) -> int:
451 | sub_files = 0 if (len(self.submodules) == 0) else \
452 | seq([s.n_of_files for s in self.submodules]).reduce(lambda a, b: a + b)
453 | return len(self.files) + sub_files
454 |
455 | @property
456 | def path(self) -> str:
457 | parent_path = "" if not self.parent else f'{self.parent.path} > '
458 | return f'{parent_path}{self.name}'
459 |
460 | @property
461 | def data(self) -> 'SyntheticData':
462 | root_module_files = [SyntheticData()] if (len(self.files) == 0) else \
463 | [SyntheticData.from_swift_file(swift_file=f) for f in self.files]
464 | submodules_files = SyntheticData() if (len(self.submodules) == 0) else \
465 | seq([s.data for s in self.submodules]).reduce(lambda a, b: a + b)
466 | return seq(root_module_files).reduce(lambda a, b: a + b) + submodules_files
467 |
468 | @property
469 | def as_dict(self) -> Dict:
470 | return {
471 | self.name: {
472 | "n_of_files": self.n_of_files,
473 | "metric": self.data.as_dict,
474 | "submodules": [s.as_dict for s in self.submodules]
475 | }
476 | }
477 |
478 |
479 | @dataclass
480 | class Dependency:
481 | name: str
482 | dependent_framework: str
483 | number_of_imports: int = 0
484 |
485 | def __eq__(self, other):
486 | return (self.name == other.name) and \
487 | (self.dependent_framework == other.dependent_framework) and \
488 | (self.number_of_imports == other.number_of_imports)
489 |
490 | def __repr__(self):
491 | return f'{self.name} - {self.dependent_framework} ({str(self.number_of_imports)}) imports'
492 |
493 | @property
494 | def compact_repr(self) -> str:
495 | return f'{self.name} ({str(self.number_of_imports)})'
496 |
497 | @property
498 | def relationship(self) -> str:
499 | return f'{self.name} > {self.dependent_framework}'
500 |
--------------------------------------------------------------------------------
/swift_code_metrics/tests/test_resources/expected_output.json:
--------------------------------------------------------------------------------
1 | {
2 | "non-test-frameworks": [
3 | {
4 | "BusinessLogic": {
5 | "loc": 51,
6 | "noc": 7,
7 | "poc": 12.069,
8 | "n_a": 0,
9 | "n_c": 3,
10 | "nom": 3,
11 | "not": 0,
12 | "noi": 2,
13 | "analysis": "The code is under commented. Low abstract component, few interfaces. ",
14 | "dependencies": [
15 | "FoundationFramework(1)",
16 | "SecretLib(1)"
17 | ],
18 | "submodules": {
19 | "BusinessLogic": {
20 | "n_of_files": 1,
21 | "metric": {
22 | "loc": 51,
23 | "noc": 7,
24 | "n_a": 0,
25 | "n_c": 3,
26 | "nom": 3,
27 | "not": 0,
28 | "poc": 12.069
29 | },
30 | "submodules": [
31 | {
32 | "BusinessLogic": {
33 | "n_of_files": 1,
34 | "metric": {
35 | "loc": 51,
36 | "noc": 7,
37 | "n_a": 0,
38 | "n_c": 3,
39 | "nom": 3,
40 | "not": 0,
41 | "poc": 12.069
42 | },
43 | "submodules": [
44 | {
45 | "BusinessLogic": {
46 | "n_of_files": 1,
47 | "metric": {
48 | "loc": 51,
49 | "noc": 7,
50 | "n_a": 0,
51 | "n_c": 3,
52 | "nom": 3,
53 | "not": 0,
54 | "poc": 12.069
55 | },
56 | "submodules": []
57 | }
58 | }
59 | ]
60 | }
61 | }
62 | ]
63 | }
64 | },
65 | "fan_in": 1,
66 | "fan_out": 2,
67 | "i": 0.667,
68 | "a": 0.0,
69 | "d_3": 0.333
70 | }
71 | },
72 | {
73 | "FoundationFramework": {
74 | "loc": 27,
75 | "noc": 21,
76 | "poc": 43.75,
77 | "n_a": 1,
78 | "n_c": 2,
79 | "nom": 3,
80 | "not": 0,
81 | "noi": 0,
82 | "analysis": "The code is over commented. Zone of Pain. Highly stable and concrete component - rigid, hard to extend (not abstract). This component should not be volatile (e.g. a stable foundation library such as Strings).",
83 | "dependencies": [],
84 | "submodules": {
85 | "FoundationFramework": {
86 | "n_of_files": 3,
87 | "metric": {
88 | "loc": 27,
89 | "noc": 21,
90 | "n_a": 1,
91 | "n_c": 2,
92 | "nom": 3,
93 | "not": 0,
94 | "poc": 43.75
95 | },
96 | "submodules": [
97 | {
98 | "Foundation": {
99 | "n_of_files": 3,
100 | "metric": {
101 | "loc": 27,
102 | "noc": 21,
103 | "n_a": 1,
104 | "n_c": 2,
105 | "nom": 3,
106 | "not": 0,
107 | "poc": 43.75
108 | },
109 | "submodules": [
110 | {
111 | "FoundationFramework": {
112 | "n_of_files": 2,
113 | "metric": {
114 | "loc": 23,
115 | "noc": 14,
116 | "n_a": 1,
117 | "n_c": 1,
118 | "nom": 2,
119 | "not": 0,
120 | "poc": 37.838
121 | },
122 | "submodules": [
123 | {
124 | "Interfaces": {
125 | "n_of_files": 1,
126 | "metric": {
127 | "loc": 12,
128 | "noc": 7,
129 | "n_a": 1,
130 | "n_c": 0,
131 | "nom": 1,
132 | "not": 0,
133 | "poc": 36.842
134 | },
135 | "submodules": []
136 | }
137 | }
138 | ]
139 | }
140 | },
141 | {
142 | "Shared": {
143 | "n_of_files": 1,
144 | "metric": {
145 | "loc": 4,
146 | "noc": 7,
147 | "n_a": 0,
148 | "n_c": 1,
149 | "nom": 1,
150 | "not": 0,
151 | "poc": 63.636
152 | },
153 | "submodules": []
154 | }
155 | }
156 | ]
157 | }
158 | }
159 | ]
160 | }
161 | },
162 | "fan_in": 1,
163 | "fan_out": 0,
164 | "i": 0.0,
165 | "a": 0.5,
166 | "d_3": 0.5
167 | }
168 | },
169 | {
170 | "SecretLib": {
171 | "loc": 12,
172 | "noc": 14,
173 | "poc": 53.846,
174 | "n_a": 0,
175 | "n_c": 2,
176 | "nom": 2,
177 | "not": 0,
178 | "noi": 0,
179 | "analysis": "The code is over commented. Zone of Pain. Highly stable and concrete component - rigid, hard to extend (not abstract). This component should not be volatile (e.g. a stable foundation library such as Strings).",
180 | "dependencies": [],
181 | "submodules": {
182 | "SecretLib": {
183 | "n_of_files": 2,
184 | "metric": {
185 | "loc": 12,
186 | "noc": 14,
187 | "n_a": 0,
188 | "n_c": 2,
189 | "nom": 2,
190 | "not": 0,
191 | "poc": 53.846
192 | },
193 | "submodules": [
194 | {
195 | "Foundation": {
196 | "n_of_files": 2,
197 | "metric": {
198 | "loc": 12,
199 | "noc": 14,
200 | "n_a": 0,
201 | "n_c": 2,
202 | "nom": 2,
203 | "not": 0,
204 | "poc": 53.846
205 | },
206 | "submodules": [
207 | {
208 | "SecretLib": {
209 | "n_of_files": 1,
210 | "metric": {
211 | "loc": 8,
212 | "noc": 7,
213 | "n_a": 0,
214 | "n_c": 1,
215 | "nom": 1,
216 | "not": 0,
217 | "poc": 46.667
218 | },
219 | "submodules": []
220 | }
221 | },
222 | {
223 | "Shared": {
224 | "n_of_files": 1,
225 | "metric": {
226 | "loc": 4,
227 | "noc": 7,
228 | "n_a": 0,
229 | "n_c": 1,
230 | "nom": 1,
231 | "not": 0,
232 | "poc": 63.636
233 | },
234 | "submodules": []
235 | }
236 | }
237 | ]
238 | }
239 | }
240 | ]
241 | }
242 | },
243 | "fan_in": 1,
244 | "fan_out": 0,
245 | "i": 0.0,
246 | "a": 0.0,
247 | "d_3": 1.0
248 | }
249 | },
250 | {
251 | "SwiftCodeMetricsExample": {
252 | "loc": 22,
253 | "noc": 14,
254 | "poc": 38.889,
255 | "n_a": 0,
256 | "n_c": 2,
257 | "nom": 4,
258 | "not": 0,
259 | "noi": 1,
260 | "analysis": "Highly unstable component (lack of dependents, easy to change, irresponsible) Low abstract component, few interfaces. ",
261 | "dependencies": [
262 | "BusinessLogic(1)"
263 | ],
264 | "submodules": {
265 | "SwiftCodeMetricsExample": {
266 | "n_of_files": 2,
267 | "metric": {
268 | "loc": 22,
269 | "noc": 14,
270 | "n_a": 0,
271 | "n_c": 2,
272 | "nom": 4,
273 | "not": 0,
274 | "poc": 38.889
275 | },
276 | "submodules": [
277 | {
278 | "SwiftCodeMetricsExample": {
279 | "n_of_files": 2,
280 | "metric": {
281 | "loc": 22,
282 | "noc": 14,
283 | "n_a": 0,
284 | "n_c": 2,
285 | "nom": 4,
286 | "not": 0,
287 | "poc": 38.889
288 | },
289 | "submodules": []
290 | }
291 | }
292 | ]
293 | }
294 | },
295 | "fan_in": 0,
296 | "fan_out": 1,
297 | "i": 1.0,
298 | "a": 0.0,
299 | "d_3": 0.0
300 | }
301 | }
302 | ],
303 | "tests-frameworks": [
304 | {
305 | "BusinessLogic_Test": {
306 | "loc": 7,
307 | "noc": 7,
308 | "poc": 50.0,
309 | "n_a": 0,
310 | "n_c": 1,
311 | "nom": 1,
312 | "not": 1,
313 | "noi": 1,
314 | "analysis": "The code is over commented. ",
315 | "dependencies": [
316 | "BusinessLogic(1)"
317 | ],
318 | "submodules": {
319 | "BusinessLogic_Test": {
320 | "n_of_files": 1,
321 | "metric": {
322 | "loc": 7,
323 | "noc": 7,
324 | "n_a": 0,
325 | "n_c": 1,
326 | "nom": 1,
327 | "not": 1,
328 | "poc": 50.0
329 | },
330 | "submodules": [
331 | {
332 | "BusinessLogic": {
333 | "n_of_files": 1,
334 | "metric": {
335 | "loc": 7,
336 | "noc": 7,
337 | "n_a": 0,
338 | "n_c": 1,
339 | "nom": 1,
340 | "not": 1,
341 | "poc": 50.0
342 | },
343 | "submodules": [
344 | {
345 | "BusinessLogicTests": {
346 | "n_of_files": 1,
347 | "metric": {
348 | "loc": 7,
349 | "noc": 7,
350 | "n_a": 0,
351 | "n_c": 1,
352 | "nom": 1,
353 | "not": 1,
354 | "poc": 50.0
355 | },
356 | "submodules": []
357 | }
358 | }
359 | ]
360 | }
361 | }
362 | ]
363 | }
364 | }
365 | }
366 | },
367 | {
368 | "FoundationFrameworkTests": {
369 | "loc": 41,
370 | "noc": 14,
371 | "poc": 25.455,
372 | "n_a": 0,
373 | "n_c": 2,
374 | "nom": 5,
375 | "not": 3,
376 | "noi": 2,
377 | "analysis": "",
378 | "dependencies": [
379 | "FoundationFramework(2)"
380 | ],
381 | "submodules": {
382 | "FoundationFrameworkTests": {
383 | "n_of_files": 2,
384 | "metric": {
385 | "loc": 41,
386 | "noc": 14,
387 | "n_a": 0,
388 | "n_c": 2,
389 | "nom": 5,
390 | "not": 3,
391 | "poc": 25.455
392 | },
393 | "submodules": [
394 | {
395 | "Foundation": {
396 | "n_of_files": 2,
397 | "metric": {
398 | "loc": 41,
399 | "noc": 14,
400 | "n_a": 0,
401 | "n_c": 2,
402 | "nom": 5,
403 | "not": 3,
404 | "poc": 25.455
405 | },
406 | "submodules": [
407 | {
408 | "FoundationFrameworkTests": {
409 | "n_of_files": 2,
410 | "metric": {
411 | "loc": 41,
412 | "noc": 14,
413 | "n_a": 0,
414 | "n_c": 2,
415 | "nom": 5,
416 | "not": 3,
417 | "poc": 25.455
418 | },
419 | "submodules": []
420 | }
421 | }
422 | ]
423 | }
424 | }
425 | ]
426 | }
427 | }
428 | }
429 | },
430 | {
431 | "SwiftCodeMetricsExampleTests_Test": {
432 | "loc": 5,
433 | "noc": 7,
434 | "poc": 58.333,
435 | "n_a": 0,
436 | "n_c": 1,
437 | "nom": 1,
438 | "not": 1,
439 | "noi": 1,
440 | "analysis": "The code is over commented. ",
441 | "dependencies": [
442 | "SwiftCodeMetricsExample(1)"
443 | ],
444 | "submodules": {
445 | "SwiftCodeMetricsExampleTests_Test": {
446 | "n_of_files": 1,
447 | "metric": {
448 | "loc": 5,
449 | "noc": 7,
450 | "n_a": 0,
451 | "n_c": 1,
452 | "nom": 1,
453 | "not": 1,
454 | "poc": 58.333
455 | },
456 | "submodules": [
457 | {
458 | "SwiftCodeMetricsExampleTests": {
459 | "n_of_files": 1,
460 | "metric": {
461 | "loc": 5,
462 | "noc": 7,
463 | "n_a": 0,
464 | "n_c": 1,
465 | "nom": 1,
466 | "not": 1,
467 | "poc": 58.333
468 | },
469 | "submodules": []
470 | }
471 | }
472 | ]
473 | }
474 | }
475 | }
476 | }
477 | ],
478 | "shared": {
479 | "loc": 4,
480 | "noc": 7,
481 | "n_a": 0,
482 | "n_c": 1,
483 | "nom": 1,
484 | "not": 0,
485 | "poc": 63.636,
486 | "noi": 0
487 | },
488 | "aggregate": {
489 | "non-test-frameworks": {
490 | "loc": 108,
491 | "noc": 49,
492 | "n_a": 1,
493 | "n_c": 8,
494 | "nom": 11,
495 | "not": 0,
496 | "poc": 31.21,
497 | "noi": 3
498 | },
499 | "tests-frameworks": {
500 | "loc": 53,
501 | "noc": 28,
502 | "n_a": 0,
503 | "n_c": 4,
504 | "nom": 7,
505 | "not": 5,
506 | "poc": 34.568,
507 | "noi": 4
508 | },
509 | "total": {
510 | "loc": 161,
511 | "noc": 77,
512 | "n_a": 1,
513 | "n_c": 12,
514 | "nom": 18,
515 | "not": 5,
516 | "poc": 32.353,
517 | "noi": 7
518 | }
519 | }
520 | }
--------------------------------------------------------------------------------