Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
HolonProduction committed Feb 3, 2023
0 parents commit e7495db
Show file tree
Hide file tree
Showing 46 changed files with 1,956 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: HolonProduction
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.venv/
.vscode/
.pytest_cache/
dist/

__pycache__
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2022-2023 HolonProduction

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# <img src="./plugins/godot/icon.svg" style="vertical-align: top; height: 1.3em" /> CodexGD - *Give your code a dress code*.

[![Godot 4.x](https://img.shields.io/static/v1?label=Godot&message=4.x&color=grey&logo=godotengine&logoColor=white&labelColor=478cbf)](https://godotengine.org)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen.svg)](https://github.com/PyCQA/pylint)

> :construction: CodexGD is currently in development. If you are looking for a way to enforce the official style guide you should have a look at [gdtoolkit](https://github.com/Scony/godot-gdscript-toolkit). I am in fact using gdtoolkit internally myself. It has its own linter that does a very good job when it comes to the official style. The main reason for me to develop this package is that I have my own GDScript style and extending gdlint is not very simple.
CodexGD is a configurable and extendable Godot style analyzer written in python. CodexGD comes with a set of rules that you can configure to your liking but it also gives you an easy way to write own rules using python.

## :electric_plug: Installation
In the future CodexGD will be available via PyPi and the Godot Asset Lib. Until then you will have to set it up manually. Start by cloning this repo.
To use the backend you will need a python 3.11 installation. Install the cloned repo as python module by calling the following command in the cloned repo's root folder.
```shell
> python -m pip install -e .
```
It is recommended to install CodexGD into a [virtual environment](https://docs.python.org/3/tutorial/venv.html). When doing this the codexgd command will not be available globaly. You will also have to configure your venv when using the editor plugin (more on this later).

To install the editor plugin copy the `plugins/godot/addons/codexgd` folder into your project's `addons` folder. You should also create a `codex.yml` file at the root of your project before enabeling it.

## :computer: General Usage
CodexGD is built around a `codex.yml` file which you will use to configure your style. The codex file will define the rules that will be used on your code. Here is a simple example:
```yml
extends: "official" # Use the official style as base.

rules:
no-invalid-chars: "warn" # Downgrade the severity of the no-invalid-chars rule.
```
The easiest way to use CodexGD is to use the provided CLI.
```shell
> python -m codexgd project_directory
```
CodexGD will look for a `codex.yml` file in the provided directory and apply it to all `.gd` files in the directory.

You can also use the package from your own python scripts. The API is centered around `Codex` objects which you can create by providing a `codex.yml` file.

```python
from codexgd.gdscript import GDScriptCodex

with open("codex.yml", "r") as file:
codex = GDScriptCodex(file)

with open("file.gd", "r") as file:
for problem in codex.check(file):
print(problem)
```

## :jigsaw: Editor Plugin
> :construction: The editor plugin is in development and not yet feature complete.
CodexGD comes with an editor integration. It will display the problems that CodexGD detected in the script editor. It relies on the CLI, therefore an installation of the python package is needed. If the python package was installed globaly the plugin should work out of the box. Otherwise you will have to select the path to the `codexgd` command in the EditorSettings `"codexgd/general/command_path"`. The file will be located in your venv folder in the `scripts` subfolder. It should be named `codexgd` with an executable file ending.

State of the editor integration:
- [x] Display problems in the script editor.
- [ ] Support for embedded scripts.
- [ ] Handle errors during the execution. (The plugin may fail silently.)
- [ ] Display overlapping problems in a good way.
- [ ] Help with the setup of a `codex.yml` file.
- [ ] Provide a simple way to see all problems of the project.

## :wrench: Configuration

### :scroll: The codex file
The codex file is a yaml file that may contain the following keys.

**`extends`**
Which style to use as base. Currently only `"official"` and `"recommended"` are supported values. You may overwrite the rule settings with the `rules` key.

**`rules`**
A list of rules with associated options.
```yaml
rules:
no-invalid-chars:
level: "warn" # The severity of the rule. Allowed values: off, warn, error
options:
codec: "utf8"
# If no options are specified the severity may be specified directly.
require-extends: "error"
```
There are multiple ways to specify which rule is meant. You can use the rule name as above but this is only possible if the rule was loaded before. You can use the name `no-invalid-chars` because the `official` base file loaded it. To load a rule you specify the python module name. You can use a global module specification like `codexgd.rules.no_invalid_chars`. You may also use relative module specifications. In this case codexgd will try to resolve them as relative path from your codex file. In this way you can load custom rules which are located in your project directory.
```yaml
rules:
.rules.custom_rule: "error"
```
The code above will load a rule from `res://rules/custom_rule.py` given your codex file is located at `res://codex.yml`.
> :warning: CodexGD will not load rules from outside the codexgd package by default because an untrusted project may execute code on your machine in this way. Pass the `--load-unsafe-code` option to the CLI to confirm that you know about the risks.

**`variables`**
A dictionary of named values which may be used in the options of rules later. This allows to unify values which are needed by multiple rules like the prefix of private methods. The name of a variable should start with `<` and end with `>`.
```yaml
variables:
<private-prefix>: "_"
rules:
function-names:
level: "warn"
options:
private-prefix: <private-prefix>
```

### :heavy_check_mark: Rules
The rules contain documentation comments explaining them. Currently implemented rules can be found at `"codexgd/rules"`.

### :see_no_evil: Ignore comments
CodexGD alows you to disable certain rules in gdscript files by using special comments.
Ignore comments are not meant to be placed on the same line with gdscript code.

```python
# Ignore only the next line.
# codexgd-ignore
# Disable rules until they are enabled again.
# codexgd-disable
# Enable rules.
# codexgd-enable
```
All ignore comments may optionally recieve a list of rule names. If no rule names are provided all rules will be affected.
```python
# codexgd-disable: function-names, no-invalid-chars
# codexgd-enable: no-invalid-chars
```

Problems are ignored based on the line on which they beginn. Therefore problems which span the complete file can only be ignored by a `codexgd-disable` statement on the first line.

It is considered good practice to state the reason for disabeling the rule behind the ignore comment in a rational and calm manner...
```python
# codexgd-ignore: no-invalid-chars Why would CodexGD not allow me to use utf8 inside of strings?! What is the developer even thinking!
```
1 change: 1 addition & 0 deletions codexgd/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
3 changes: 3 additions & 0 deletions codexgd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .problem import Problem
from .exceptions import CodexGDError
from .codex import Codex
138 changes: 138 additions & 0 deletions codexgd/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""CodexGD
A configurable and extendable Godot style analyzer.
CodexGD can also be used in your own python scripts
with an API that is more powerfull than this CLI.
Usage:
codexgd <dir> [options]
codexgd <codex> <gdscript>... [options]
Options:
-h --help Show this screen.
-v --version Show the current version.
--encoding=codec The encoding of the files. [default: "utf-8"]
--load-unsafe-code Load untrusted code from outside the CodexGD package if the codex file says so.
--json Output json data.
--
Returns:
0 The input conforms to the given codex file.
1 The input does not conform to the give codex.
2 During execution an exception occured.
"""

import os.path
import sys
import glob
import json
from docopt import docopt
from codexgd import CodexGDError
from codexgd.gdscript import GDScriptCodex
from codexgd.__about__ import __version__


def main():
arguments = docopt(__doc__, version=__version__)

if arguments["<dir>"]:
try:
with open(
os.path.join(arguments["<dir>"], "codex.yml"),
"r",
encoding=arguments["--encoding"],
) as file:
codex = GDScriptCodex(file, arguments["--load-unsafe-code"])
except (FileExistsError, CodexGDError) as error:
if not arguments["--json"]:
print(error)
sys.exit(2)
elif arguments["<codex>"]:
try:
with open(
arguments["<codex>"], "r", encoding=arguments["--encoding"]
) as file:
codex = GDScriptCodex(file, arguments["--load-unsafe-code"])
except (FileExistsError, CodexGDError) as error:
if not arguments["--json"]:
print(error)
sys.exit(2)
else:
if not arguments["--json"]:
print("No configuration was provided.")
sys.exit(2)

file_paths = []
file_paths += arguments["<gdscript>"]

if arguments["<dir>"]:
file_paths += glob.glob(
os.path.join(arguments["<dir>"], "**", "*.gd"), recursive=True
)

if arguments["--json"]:
print("[", end="")

found_problems = 0
for file_path in file_paths:
with open(file_path, "r", encoding=arguments["--encoding"]) as file:
for problem in codex.check(file):
if arguments["--json"]:
if found_problems > 0:
print(", ", end="")
print(
json.dumps(
{
"severity": problem.rule.severity,
"info": problem.info,
"file": file_path,
"start": problem.start,
"end": problem.end,
"rule": problem.rule.name,
}
),
end="",
)
else:
print(
problem.rule.severity.upper(),
": ",
problem.info,
"\t",
os.path.realpath(file_path),
" Ln ",
problem.start[0],
":",
problem.start[1],
" (",
problem.rule.name,
")",
sep="",
)
found_problems += 1

if not arguments["--json"]:
print(
"\n" if found_problems > 0 else "",
"CodexGD found ",
str(found_problems),
" problem",
"s" if found_problems != 1 else "",
" in ",
str(len(file_paths)),
" file",
"s" if len(file_paths) > 1 else "",
".",
" Go fix " + ("them" if found_problems > 1 else "it") + "!"
if found_problems > 0
else " \U0001f389",
sep="",
)
else:
print("]", end="")

sys.exit(int(found_problems > 0))


if __name__ == "__main__":
main()
32 changes: 32 additions & 0 deletions codexgd/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import TypeVar, TypeVarTuple, Generic, Self, Type

Values = TypeVarTuple("Values")


class Callback(Generic[*Values]):
"""A callback allows the rules to hook into some processing step of the codex."""


Identifier = TypeVar("Identifier")


class DynamicCallback(Callback[*Values], Generic[*Values, Identifier]):
"""
A dynamic callback allows to pass an identifier.
This allows the codex to notify only certain group of listeners.
"""

_identifier: Identifier

def __init__(self, identifier: Identifier) -> None:
self._identifier = identifier

def __eq__(self, other: "DynamicCallback") -> bool:
return self._identifier == other._identifier

def __hash__(self) -> int:
return hash((self.__class__, self._identifier))

@classmethod
def factory(cls: Type[Self], identifier: Identifier) -> Self:
return cls(identifier)
Loading

0 comments on commit e7495db

Please sign in to comment.