Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
jobs:
build:
name: Tests
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
Expand Down
25 changes: 15 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Prefer a pinned image which is used in other Dockerfiles.
# alpine:3.23.6@sha256:85fe1e81d6758c208f3e1eed4338a1997e19d4be002d4dd32d3100c9a8c010a0
# alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
# debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
# ubuntu:26.04@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
# ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55
FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6

# install packages required to run the tests
RUN apk add --no-cache coreutils
RUN apk add --no-cache python3 py3-pip
RUN pip install --user --break-system-packages 'gdtoolkit == 4.5.0'
# gdtoolkit pins an older lark release. Force a newer version.
RUN pip install --user --break-system-packages --no-deps 'lark == 1.3.1'

# Load and cache the gdscipt grammar
RUN python3 -c 'import gdtoolkit.parser; gdtoolkit.parser.parser.parse("")'

# Everything prior to this line should be synced with gdscript-representer/Dockerfile

WORKDIR /opt/analyzer
COPY . .
ENTRYPOINT ["/opt/analyzer/bin/run.sh"]

COPY gdlintrc gdlintrc
COPY bin/run.sh bin/run.sh
COPY bin/run.py bin/run.py

ENTRYPOINT ["/opt/representer/bin/run.sh"]
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

The Docker image to automatically run tests on GDScript solutions submitted to [Exercism].

## Getting started
## Implementation

Build the analyzer, conforming to the [analyzer interface specification](https://github.com/exercism/docs/blob/main/building/tooling/analyzers/interface.md).
Update the files to match your track's needs.
At the very least, you'll need to update `bin/run.sh`, `Dockerfile` and the test solutions in the `tests` directory

- Tip: look for `TODO:` comments to point you towards code that need updating
- Tip: look for `OPTIONAL:` comments to point you towards code that _could_ be useful
The analyzer wraps the `gdlint` logic from `gdtoolkit` and outputs the results matching the Exercism interface.

## Run the analyzer

Expand Down
2 changes: 1 addition & 1 deletion bin/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ for test_dir in tests/*; do
expected_file="expected_${file}"
echo "${test_dir_name}: comparing ${file} to ${expected_file}"

if ! diff "${test_dir_path}/${file}" "${test_dir_path}/${expected_file}"; then
if ! diff "${test_dir_path}/${expected_file}" "${test_dir_path}/${file}"; then
exit_code=1
fi
done
Expand Down
84 changes: 84 additions & 0 deletions bin/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Generate a representation of a GDScript solution."""

import argparse
import dataclasses
import json
import pathlib
import yaml

import gdtoolkit.linter


def gdlintrc() -> dict:
"""Load the gdlint config."""
path = pathlib.Path("gdlintrc")
if path.exists():
return yaml.load(pathlib.Path("gdlintrc").read_text(), Loader=yaml.Loader)
return gdtoolkit.linter.DEFAULT_CONFIG


def load_config(input_path: pathlib.Path) -> dict | None:
"""Return the contents of .meta/config.json if it exists, or None."""
config_path = input_path / ".meta/config.json"
if not config_path.exists():
return None
return json.loads((input_path / ".meta/config.json").read_text())


def solution_files(slug: str, input_path: pathlib.Path) -> list[pathlib.Path]:
"""Return the solution files, based on the config.json if available, or the slug."""
config = load_config(input_path)
if config:
files = config.get("files", {}).get("solution", [])
if files:
return [input_path / file for file in files]
return [(input_path / slug.replace("-", "_")).with_suffix(".gd")]


def format_comments(path: pathlib.Path, problems: list[gdtoolkit.linter.problem.Problem]) -> list[dict[str, str]]:
return [
{"comment": "gdscript.gdlint.finding", "params": dataclasses.asdict(problem) | {"file": path.name}}
for problem in problems
]


def write_output(output_path: pathlib.Path, comments: list[dict], tags: list[str]) -> None:
"""Write the representer files."""
if not output_path.exists():
output_path.mkdir(parents=True, exist_ok=True)

(output_path / "tags.json").write_text(json.dumps({"tags": tags}, indent=2) + "\n")
(output_path / "analysis.json").write_text(json.dumps({"comments": comments}, indent=2, sort_keys=True) + "\n")


def arg_parser() -> argparse.ArgumentParser:
"""Return an ArgumentParser."""
parser = argparse.ArgumentParser()
parser.add_argument("slug")
parser.add_argument("input_dir")
parser.add_argument("output_dir")
return parser


def main() -> None:
"""Generate a representation."""
args = arg_parser().parse_args()
slug = args.slug
input_path = pathlib.Path(args.input_dir)
output_path = pathlib.Path(args.output_dir)

print(f"== Analyzing {args.slug!r} ==")
comments = []
config = gdlintrc()
for solution_file in solution_files(slug, input_path):
comments.extend(format_comments(solution_file, gdtoolkit.linter.lint_code(solution_file.read_text())))

tags = []

write_output(output_path, comments, tags)
print(f"== Done {args.slug!r} ==")


if __name__ == "__main__":
main()
26 changes: 1 addition & 25 deletions bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,4 @@
# Example:
# ./bin/run.sh two-fer path/to/solution/folder/ path/to/output/directory/

# If any required arguments is missing, print the usage and exit
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
echo "usage: ./bin/run.sh exercise-slug path/to/solution/folder/ path/to/output/directory/"
exit 1
fi

slug="$1"
solution_dir=$(realpath "${2%/}")
output_dir=$(realpath "${3%/}")
analysis_file="${output_dir}/analysis.json"
tags_file="${output_dir}/tags.json"

# Create the output directory if it doesn't exist
mkdir -p "${output_dir}"

echo "${slug}: analyzing..."

# Analyze the solution
# TODO: replace the below command with your own command(s)
# to analyze the solution and output the analysis.json and
# tags.json files
echo '{"comments": []}' > "${analysis_file}"
echo '{"tags": []}' > "${tags_file}"

echo "${slug}: done"
python3 "${0%.*}.py" "$@"
47 changes: 47 additions & 0 deletions gdlintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class-definitions-order:
- tools
- classnames
- extends
- docstrings
- signals
- enums
- consts
- staticvars
- exports
- pubvars
- prvvars
- onreadypubvars
- onreadyprvvars
- others
class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
class-name: ([A-Z][a-z0-9]*)+
class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
comparison-with-itself: null
constant-name: _?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*
disable: []
duplicated-load: null
enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'
enum-name: ([A-Z][a-z0-9]*)+
excluded_directories: !!set
.git: null
expression-not-assigned: null
function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
function-arguments-number: 10
function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
function-preload-variable-name: ([A-Z][a-z0-9]*)+
function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
load-constant-name: (([A-Z][a-z0-9]*)+|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)
loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
max-file-lines: 1000
max-line-length: 120
max-public-methods: 100
max-returns: 8
mixed-tabs-and-spaces: null
no-elif-return: null
no-else-return: null
signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
sub-class-name: _?([A-Z][a-z0-9]*)+
tab-characters: 1
trailing-whitespace: null
unnecessary-pass: null
unused-argument: null
7 changes: 7 additions & 0 deletions tests/else-return/else_return.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
func foo(a):
if a == 1:
return "One"
elif a == 2:
return "Two"
else:
return "Three"
24 changes: 24 additions & 0 deletions tests/else-return/expected_analysis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"comments": [
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 2,
"description": "Unnecessary \"elif\" after \"return\"",
"file": "else_return.gd",
"line": 4,
"name": "no-elif-return"
}
},
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 2,
"description": "Unnecessary \"else\" after \"return\"",
"file": "else_return.gd",
"line": 6,
"name": "no-else-return"
}
}
]
}
3 changes: 3 additions & 0 deletions tests/else-return/expected_tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tags": []
}
34 changes: 34 additions & 0 deletions tests/naming/expected_analysis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"comments": [
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 6,
"description": "Function name \"Foo\" is not valid",
"file": "naming.gd",
"line": 3,
"name": "function-name"
}
},
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 6,
"description": "Function-scope variable name \"Name\" is not valid",
"file": "naming.gd",
"line": 4,
"name": "function-variable-name"
}
},
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 7,
"description": "Constant name \"Five\" is not valid",
"file": "naming.gd",
"line": 1,
"name": "constant-name"
}
}
]
}
3 changes: 3 additions & 0 deletions tests/naming/expected_tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tags": []
}
4 changes: 4 additions & 0 deletions tests/naming/naming.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const Five = 5

func Foo():
var Name = "bob"
4 changes: 3 additions & 1 deletion tests/no-comments/expected_analysis.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{"comments": []}
{
"comments": []
}
4 changes: 3 additions & 1 deletion tests/no-comments/expected_tags.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{"tags": []}
{
"tags": []
}
2 changes: 2 additions & 0 deletions tests/no-comments/no_comments.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
func foo():
return 1
14 changes: 14 additions & 0 deletions tests/trailing-whitespace/expected_analysis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"comments": [
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 0,
"description": "Trailing whitespace(s)",
"file": "trailing_whitespace.gd",
"line": 2,
"name": "trailing-whitespace"
}
}
]
}
3 changes: 3 additions & 0 deletions tests/trailing-whitespace/expected_tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tags": []
}
2 changes: 2 additions & 0 deletions tests/trailing-whitespace/trailing_whitespace.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
func foo():
return 1
14 changes: 14 additions & 0 deletions tests/unused-arg/expected_analysis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"comments": [
{
"comment": "gdscript.gdlint.finding",
"params": {
"column": 10,
"description": "unused function argument 'bob'",
"file": "unused_arg.gd",
"line": 1,
"name": "unused-argument"
}
}
]
}
3 changes: 3 additions & 0 deletions tests/unused-arg/expected_tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tags": []
}
2 changes: 2 additions & 0 deletions tests/unused-arg/unused_arg.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
func foo(bob):
return 1
Loading