├── .coveragerc ├── .github ├── FUNDING.yml └── workflows │ └── deploy.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── build_and_test.sh ├── codecov.sh ├── docs ├── GUIDE.md └── assets │ ├── code_distribution.jpeg │ ├── code_distribution_submodules.jpeg │ ├── example_deviation_main_sequence.jpeg │ ├── example_internal_deps_graph.jpeg │ ├── lines_of_code_-_loc.jpeg │ ├── n._of_classes_and_structs.jpeg │ ├── n._of_imports_-_noi.jpeg │ ├── number_of_comments_-_noc.jpeg │ ├── number_of_tests_-_not.jpeg │ └── scm_override_example.png ├── install.sh ├── requirements.txt ├── setup.py ├── swift-code-metrics-runner.py ├── swift_code_metrics ├── __init__.py ├── __main__.py ├── _analyzer.py ├── _graph_helpers.py ├── _graphs_presenter.py ├── _graphs_renderer.py ├── _helpers.py ├── _metrics.py ├── _parser.py ├── _report.py ├── scm.py ├── tests │ ├── __init__.py │ ├── test_helper.py │ ├── test_integration.py │ ├── test_metrics.py │ ├── test_parser.py │ └── test_resources │ │ ├── ExampleFile.swift │ │ ├── ExampleProject │ │ └── SwiftCodeMetricsExample │ │ │ ├── BusinessLogic │ │ │ ├── BusinessLogic.xcodeproj │ │ │ │ ├── project.pbxproj │ │ │ │ └── project.xcworkspace │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ ├── BusinessLogic │ │ │ │ ├── AwesomeFeature.swift │ │ │ │ └── Info.plist │ │ │ └── BusinessLogicTests │ │ │ │ ├── AwesomeFeatureViewControllerTests.swift │ │ │ │ └── Info.plist │ │ │ ├── Foundation │ │ │ ├── Foundation.xcodeproj │ │ │ │ ├── project.pbxproj │ │ │ │ └── project.xcworkspace │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ ├── FoundationFramework │ │ │ │ ├── Info.plist │ │ │ │ ├── Interfaces │ │ │ │ │ └── CommonTypes.swift │ │ │ │ └── Networking.swift │ │ │ ├── FoundationFrameworkTests │ │ │ │ ├── FoundationFrameworkTests.swift │ │ │ │ ├── Info.plist │ │ │ │ └── NetworkingTests.swift │ │ │ ├── SecretLib │ │ │ │ ├── CaesarChiper.swift │ │ │ │ └── Info.plist │ │ │ ├── Shared │ │ │ │ └── Helpers.swift │ │ │ └── scm.json │ │ │ ├── SwiftCodeMetricsExample.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── SwiftCodeMetricsExample.xcscheme │ │ │ ├── SwiftCodeMetricsExample │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── ViewController.swift │ │ │ └── SwiftCodeMetricsExampleTests │ │ │ ├── Info.plist │ │ │ └── SwiftCodeMetricsExampleTests.swift │ │ ├── ExampleTest.swift │ │ ├── expected_output.json │ │ └── scm_overrides │ │ ├── invalid_scm_override.json │ │ └── valid_scm_override.json └── version.py └── tests-requirements.txt /.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 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | liberapay: matsoftware 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmatsoftware%2Fswift-code-metrics.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmatsoftware%2Fswift-code-metrics?ref=badge_shield) [![License](https://img.shields.io/badge/license-MIT-blue.svg?x=1)](LICENSE) [![Build Status](https://travis-ci.org/matsoftware/swift-code-metrics.svg?branch=master)](https://travis-ci.org/matsoftware/swift-code-metrics) [![codecov](https://codecov.io/gh/matsoftware/swift-code-metrics/branch/master/graph/badge.svg)](https://codecov.io/gh/matsoftware/swift-code-metrics) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/c4ecbd3b64cf4518a113bb56d93f6323)](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 | [![PyPI](https://img.shields.io/pypi/v/swift-code-metrics.svg)](https://pypi.python.org/pypi/swift-code-metrics) 3 | 4 | # swift-code-metrics 5 | 6 | Code metrics analyzer for Swift projects. 7 | 8 | | ![Example code distribution](https://raw.githubusercontent.com/matsoftware/swift-code-metrics/master/docs/assets/code_distribution.jpeg) ![Example deviation main sequence](https://raw.githubusercontent.com/matsoftware/swift-code-metrics/master/docs/assets/example_deviation_main_sequence.jpeg) | 9 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 10 | | ![Example internal distribution](https://raw.githubusercontent.com/matsoftware/swift-code-metrics/master/docs/assets/example_internal_deps_graph.jpeg) | 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 | -------------------------------------------------------------------------------- /build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | top="$(dirname "$0")" 6 | 7 | "${top}"/venv/bin/pytest --cov=swift_code_metrics -------------------------------------------------------------------------------- /codecov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | top="$(dirname "$0")" 6 | 7 | "${top}"/venv/bin/codecov -------------------------------------------------------------------------------- /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 | ![SCM Override](assets/scm_override_example.png) 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 | ![Dependency graph](assets/example_internal_deps_graph.jpeg) 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 | ![Distance from main sequence](assets/example_deviation_main_sequence.jpeg) 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 | ![LOC](assets/lines_of_code_-_loc.jpeg) ![NOC](assets/number_of_comments_-_noc.jpeg) ![Nc](assets/n._of_classes_and_structs.jpeg) ![NOT](assets/number_of_tests_-_not.jpeg) ![Code distribution](assets/code_distribution.jpeg) ![Code distribution in submodules](assets/code_distribution_submodules.jpeg) 289 | -------------------------------------------------------------------------------- /docs/assets/code_distribution.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/code_distribution.jpeg -------------------------------------------------------------------------------- /docs/assets/code_distribution_submodules.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/code_distribution_submodules.jpeg -------------------------------------------------------------------------------- /docs/assets/example_deviation_main_sequence.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/example_deviation_main_sequence.jpeg -------------------------------------------------------------------------------- /docs/assets/example_internal_deps_graph.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/example_internal_deps_graph.jpeg -------------------------------------------------------------------------------- /docs/assets/lines_of_code_-_loc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/lines_of_code_-_loc.jpeg -------------------------------------------------------------------------------- /docs/assets/n._of_classes_and_structs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/n._of_classes_and_structs.jpeg -------------------------------------------------------------------------------- /docs/assets/n._of_imports_-_noi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/n._of_imports_-_noi.jpeg -------------------------------------------------------------------------------- /docs/assets/number_of_comments_-_noc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/number_of_comments_-_noc.jpeg -------------------------------------------------------------------------------- /docs/assets/number_of_tests_-_not.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/number_of_tests_-_not.jpeg -------------------------------------------------------------------------------- /docs/assets/scm_override_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/docs/assets/scm_override_example.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /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-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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/swift_code_metrics/__init__.py -------------------------------------------------------------------------------- /swift_code_metrics/__main__.py: -------------------------------------------------------------------------------- 1 | from .scm import main 2 | main() 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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/_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/_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 | -------------------------------------------------------------------------------- /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/_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/_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 | -------------------------------------------------------------------------------- /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/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsoftware/swift-code-metrics/d05f77f55e64473d87bca5c953f33b98c01dc6e8/swift_code_metrics/tests/__init__.py -------------------------------------------------------------------------------- /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/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_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_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/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/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/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/BusinessLogic/BusinessLogic.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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/Foundation.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/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/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/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/SwiftCodeMetricsExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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_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/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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /swift_code_metrics/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.5.4" 2 | -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------