Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Filter functions with # nocl comments #24

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 22 additions & 210 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,234 +18,46 @@ Your Refactoring Alarm 🔔
Code Limit is a tool for developers with one goal: _it tells the developer when
it’s time to refactor_.

Code Limit measures the lines of code for each function in your codebase and
assigns each function to a category:

<div align="center">

| Easy | Verbose | Hard-to-maintain 🔔 | Unmaintainable 🚨 |
| :---: | :---: | :---: | :---: |
| 1 - 15 lines of code | 16 - 30 lines of code | 31 - 60 lines of code | 60+ lines of code |
| ![](https://raw.githubusercontent.com/getcodelimit/codelimit/main/docs/assets/easy.png) | ![](https://raw.githubusercontent.com/getcodelimit/codelimit/main/docs/assets/verbose.png) | ![](https://raw.githubusercontent.com/getcodelimit/codelimit/main/docs/assets/hard-to-maintain.png) | ![](https://raw.githubusercontent.com/getcodelimit/codelimit/main/docs/assets/unmaintainable.png) |

</div>

As the table above shows, functions with more than 60 lines of code (comments
and empty lines are not counted) are _unmaintainable_, and _need_ to be
refactored. Functions with more than 30 lines of code run a risk of turning
into unmaintainable functions over time, you should keep an eye on them and
refactor if possible. Functions in the first two categories are fine and don't
need refactoring.

Function length is just one code metric, but it is a very important code
metric. Short functions are easy to understand, easy to test, easy to re-use.
For example, code duplication is another important code metric but duplication
is much easier to prevent and fix if your functions are short.

Function length is a simple code metric, so simple you can count it by hand.
Because it's such a simple metric, it's also a (fairly) non-controversial
metric. Most developers agree longer functions are harder to maintain. Also,
there's always a refactoring possible to make functions smaller.

Because function length is such a simple code metric, many code quality tools
measure it. But these tools measure a lot more metrics, sometimes so much
metrics that developers are overwhemled and loose focus on the most important
metric: function length.

Code Limit measures only function length but it tries to be the best developer
tool for measuring it. By notifying developers when it's time to refactor, Code
Limit prevents unmaintainable code.

Let's keep your software maintainable and start using Code Limit today!
Keep your code maintainable and start using Code Limit today!

# Quickstart

Depending on your development workflow, Code Limit can run as:

- [Pre-commit hook](#pre-commit-hook)
- [GitHub Action](#github-action)
- Standalone
- [Homebrew](#homebrew-install)
- [Pipx](#pipx-install)
- [Pypi](#pypi-install)
- [Platform binary](#platform-binaries)

## Pre-commit hook

Code Limit can be installed as a [pre-commit](https://pre-commit.com/) hook so
it alarms you during development when it's time to refactor:

```yaml
- repo: https://github.com/getcodelimit/codelimit
rev: 0.6.2
hooks:
- id: codelimit
```

Code Limit is intended to be used alongside formatting, linters and other hooks
that improve the consistency and quality of your code (such as
[Black](https://github.com/psf/black),
[Ruff](https://github.com/astral-sh/ruff) and
[MyPy](https://github.com/python/mypy).) As an example pre-commit configuration
see the
[`pre-commit-config.yaml`](https://github.com/getcodelimit/codelimit/blob/main/.pre-commit-config.yaml)
from Code Limit itself.

When running as a hook, Code Limit *warns* about functions that *should* be
refactored and *fails* for functions that *need* to be refactord.

To show your project uses Code Limit place this badge in the README markdown:
```
![Checked with Code Limit](https://img.shields.io/badge/CodeLimit-checked-green.svg)](https://github.com/getcodelimit/codelimit)
```

## GitHub Action

Code Limit is available as a GitHub Action

To run Code Limit on every push and before every merge to `main`, append it to
your GH Action workflow:

```yaml
name: 'main'
on:
push:
branches: main
pull_request:
branches: main
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v2
- name: 'Run Code Limit'
uses: getcodelimit/codelimit-action@main
```

## Standalone

Code Limit can also run as a standalone program.

### Homebrew install

Code Limit is available on
[Homebrew](https://formulae.brew.sh/formula/codelimit):

```shell
brew install codelimit
```

### Pipx install

To install the standalone version of Code Limit in an isolated Python
environment using [pipx](https://pypa.github.io/pipx) run:

```
pipx install codelimit
```

### PyPi install

To install the standalone version of Code Limit for your default Python
installation run:

```shell
python -m pip install codelimit
```

### Platform binaries

Binaries for different platforms (macOS, Linux, Windows) are available on the
[latest release
page](https://github.com/getcodelimit/codelimit/releases/latest).
Depending on your development workflow, Code Limit can run in many different
ways (e.g.: pre-commit hook, GitHub Action, standalone, etc.). See the
[Quickstart documentation](https://codelimit-docs.vercel.app/quickstart/) for
examples.

# Standalone usage

Run Code Limit without arguments to see the usage page:

```shell
$ codelimit

Usage: codelimit [OPTIONS] COMMAND [ARGS]...
Code Limit can run as a standalone program to check and inspect a codebase, see
the [Standalone Usage documentation](https://codelimit-docs.vercel.app/usage/)
to get started.

Code Limit: Your refactoring alarm
# Configuration

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ check Check file(s) │
│ scan Scan a codebase │
╰──────────────────────────────────────────────────────────────────────────────╯
```

## Scanning a codebase

To scan a complete codebase and launch the TUI, run:

```shell
codelimit scan path/to/codebase
```

![Screenshot](https://raw.githubusercontent.com/getcodelimit/codelimit/main/docs/assets/screenshot.png)

## Checking files

To check a single file or list of files for functions that need refactoring,
run:

```shell
codelimit check a.py b.py c.py
```

# Development

After installing dependencies with `poetry install`, Code Limit can be run from the
repository root like this:
See the [Development
documentation](https://codelimit-docs.vercel.app/development) if you want to
extend or contribute to Code Limit.

```shell
poetry run codelimit
```

For example, to check a codebase at `~/projects/fastapi` run:

```shell
poetry run codelimit ~/projects/fastapi
```

## Using the Textal debug console
# Feedback, suggestions and bug reports

Open a terminal and start the Textual debug console:
If you have suggestions for how Code Limit could be improved, or want to report
a bug, [open an issue](https://github.com/getcodelimit/codelimit/issues)! All
and any contributions are appreciated.

```shell
poetry run textual console
```

Next, open another terminal and start Code Limit in development mode:

```shell
poetry run textual run --dev main.py
```

## Building the binary distribution
To show your project uses Code Limit place this badge in the README markdown:

Generate a self-contained binary:
[![Checked with Code Limit](https://img.shields.io/badge/CodeLimit-checked-green.svg)](https://github.com/getcodelimit/codelimit)

```shell
poetry run poe bundle
```

## Static documentation

Generating the static documentation:

```shell
poetry run mkdocs build
![Checked with Code Limit](https://img.shields.io/badge/CodeLimit-checked-green.svg)](https://github.com/getcodelimit/codelimit)
```

See the output:
# License

```shell
poetry run mkdocs serve
```
[ISC](LICENSE) © 2022 Rob van der Leek <[email protected]>
(https://twitter.com/robvanderleek)
12 changes: 5 additions & 7 deletions codelimit/common/Language.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def get_lexer(self) -> Lexer:
def get_scope_extractor(self) -> ScopeExtractor:
pass

def lex(self, code: str) -> list[Token]:
def lex(self, code: str, filter_comments=True) -> list[Token]:
lexer = self.get_lexer()
lexer_tokens = lexer.get_tokens_unprocessed(code)
indices = get_newline_indices(code)
Expand All @@ -46,9 +46,7 @@ def lex(self, code: str) -> list[Token]:
)
)

def predicate(token: Token):
if token.is_whitespace() or token.is_comment():
return False
return True

return filter_tokens(tokens, predicate)
if filter_comments:
return filter_tokens(tokens)
else:
return filter_tokens(tokens, keep_comments=True)
28 changes: 26 additions & 2 deletions codelimit/common/scope/scope_utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from typing import Optional

from codelimit.common.Language import Language
from codelimit.common.Token import Token
from codelimit.common.scope.Header import Header
from codelimit.common.scope.Scope import Scope
from codelimit.common.TokenRange import TokenRange
from codelimit.common.source_utils import filter_tokens, filter_nocl_comment_tokens
from codelimit.common.token_utils import sort_tokens
from codelimit.common.utils import delete_indices


def build_scopes(language: Language, code: str) -> list[Scope]:
tokens = language.lex(code)
all_tokens = language.lex(code, False)
tokens = filter_tokens(all_tokens)
nocl_comment_tokens = filter_nocl_comment_tokens(all_tokens)
scope_extractor = language.get_scope_extractor()
headers = scope_extractor.extract_headers(tokens)
blocks = scope_extractor.extract_blocks(tokens, headers)
return _build_scopes_from_headers_and_blocks(headers, blocks)
scopes = _build_scopes_from_headers_and_blocks(headers, blocks)
return _filter_nocl_scopes(scopes, nocl_comment_tokens)


def _build_scopes_from_headers_and_blocks(
Expand Down Expand Up @@ -56,3 +61,22 @@ def _get_closest_block(
if block.gt(header):
return block
return None


def _filter_nocl_scopes(
scopes: list[Scope], nocl_comment_tokens: list[Token]
) -> list[Scope]:
nocl_comment_lines = [t.location.line for t in nocl_comment_tokens]

def get_scope_header_lines(scope: Scope) -> set[int]:
result = set([t.location.line for t in scope.header.token_range.tokens])
first_line = scope.header.token_range.tokens[0].location.line
if first_line > 0:
result.add(first_line - 1)
return result

return [
s
for s in scopes
if len(get_scope_header_lines(s).intersection(nocl_comment_lines)) == 0
]
23 changes: 20 additions & 3 deletions codelimit/common/source_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Callable

from codelimit.common.Location import Location
from codelimit.common.Token import Token

Expand Down Expand Up @@ -50,6 +48,25 @@ def get_location_range(code: str, start: Location, end: Location) -> str:


def filter_tokens(
tokens: list[Token], predicate: Callable[[Token], bool]
tokens: list[Token], keep_whitespace=False, keep_comments=False, keep_others=True
) -> list[Token]:
def predicate(token: Token):
if token.is_whitespace():
return keep_whitespace
elif token.is_comment():
return keep_comments
else:
return keep_others

return [t for t in tokens if predicate(t)]


def filter_nocl_comment_tokens(tokens: list[Token]):
def predicate(token: Token):
if token.is_comment():
value = token.value.lower()
return value.startswith("#nocl") or value.startswith("# nocl")
else:
return False

return [t for t in tokens if predicate(t)]
Binary file added docs/assets/unmaintainable-code.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Configuration

## Ignoring functions

Functions can be excluded from analysis by putting a `# nocl` comment on the
line above the start of the funtion, or any line of the function header.

For example, to ignore a function with a `# nocl` comment above the start of
the funtions:

```python
# nocl
def some_function():
...
```

Or you can ignore a function by putting a `# nocl` comment on any line of the
header:

```python
def some_function(): # nocl
...
```

```python
def some_functions(
some_numbers: list[int]
) -> int: # nocl
...
```
Loading
Loading