Skip to content

[1.0]: Initial version #3

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git/
.appends/
.github/
.gitattributes
.gitignore
Dockerfile
bin/run-in-docker.sh
bin/run-tests-in-docker.sh
bin/run-tests.sh
tests/
31 changes: 31 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# Serialisation
*.json text

# Text files where line endings should be preserved
*.patch -text

# Docker
Dockerfile text

# Documentation
*.markdown text
*.md text
*.txt text
LICENSE text
*README* text

#
# Exclude files from exporting
#

.gitattributes export-ignore
.gitignore export-ignore
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

jobs:
build:
name: Tests
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db
with:
install: true
- name: Build Docker image and store in cache
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85
with:
context: .
push: false
load: true
tags: exercism/swift-analyzer
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Tests in Docker
run: bin/run-tests-in-docker.sh
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/tests/**/*/analysis.json
/tests/**/*/tags.json

.build/
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"configurations": [
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:swift-analyzer}",
"name": "Debug Analyzer",
"program": "${workspaceFolder:swift-analyzer}/.build/debug/Analyzer",
"preLaunchTask": "swift: Build Debug Analyzer"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:swift-analyzer}",
"name": "Release Analyzer",
"program": "${workspaceFolder:swift-analyzer}/.build/release/Analyzer",
"preLaunchTask": "swift: Build Release Analyzer"
}
]
}
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM swift:6.1.2-noble AS builder
WORKDIR /opt/analyzer
COPY ./Sources/ ./Sources/
COPY ./Package.swift ./Package.resolved ./

# Print Installed Swift Version
RUN swift --version
#RUN swift package clean
RUN swift build --configuration release

FROM swift:6.1.2-noble-slim
RUN apt-get update && apt-get install -y jq
WORKDIR /opt/analyzer
COPY bin/ bin/
COPY --from=builder /opt/analyzer/.build/release/Analyzer bin/

ENTRYPOINT ["./bin/run.sh"]
51 changes: 51 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Analyzer",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.executable(
name: "Analyzer",
targets: ["Analyzer"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/swiftlang/swift-format.git", exact: "601.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "Analyzer",
dependencies: [
.product(name: "SwiftFormat", package: "swift-format"),
])
]
)
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Exercism Swift analyzer

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

## Getting started

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

## Run the analyzer

To analyze an arbitrary exercise, do the following:

1. Open a terminal in the project's root
2. Run `./bin/run.sh <exercise-slug> <solution-dir> <output-dir>`

Once the analyzer has finished, its results will be written to `<output-dir>/analysis.json`.

## Run the analyzer on an exercise using Docker

_This script is provided for testing purposes, as it mimics how analyzers run in Exercism's production environment._

To analyze an arbitrary exercise using the Docker image, do the following:

1. Open a terminal in the project's root
2. Run `./bin/run-in-docker.sh <exercise-slug> <solution-dir> <output-dir>`

Once the analyzer has finished, its results will be written to `<output-dir>/analysis.json`.

## Run the tests

To run the tests to verify the behavior of the analyzer, do the following:

1. Open a terminal in the project's root
2. Run `./bin/run-tests.sh`

These are [golden tests][golden] that compare the `analysis.json` generated by running the current state of the code against the "known good" `tests/<test-name>/expected_analysis.json`. All files created during the test run itself are discarded.

When you've made modifications to the code that will result in a new "golden" state, you'll need to generate and commit a new `tests/<test-name>/expected_analysis.json` file.

## Run the tests using Docker

_This script is provided for testing purposes, as it mimics how analyzers run in Exercism's production environment._

To run the tests to verify the behavior of the analyzer using the Docker image, do the following:

1. Open a terminal in the project's root
2. Run `./bin/run-tests-in-docker.sh`

These are [golden tests][golden] that compare the `analysis.json` generated by running the current state of the code against the "known good" `tests/<test-name>/expected_analysis.json`. All files created during the test run itself are discarded.

When you've made modifications to the code that will result in a new "golden" state, you'll need to generate and commit a new `tests/<test-name>/expected_analysis.json` file.

## Benchmarking

There are two scripts you can use to benchmark the analyzer:

1. `./bin/benchmark.sh`: benchmark the analyzer code
2. `./bin/benchmark-in-docker.sh`: benchmark the Docker image

These scripts can give a rough estimation of the analyzer's performance.
Bear in mind though that the performance on Exercism's production servers is often lower.

[analyzers]: https://github.com/exercism/docs/tree/main/building/tooling/analyzers
[golden]: https://ro-che.info/articles/2017-12-04-golden-tests
[exercism]: https://exercism.io
20 changes: 20 additions & 0 deletions Sources/Analyzer/Analysis.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

typealias Param = Dictionary<String, String>

struct Analysis : Codable {
var comments: [Comment] = []
}

enum Kind : String, Codable {
case informative
case actionable
case essential
case celebratory
}

struct Comment : Codable {
var comment: String
var params: Param
var type : Kind
}
32 changes: 32 additions & 0 deletions Sources/Analyzer/Analyzer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import SwiftFormat

class Analyzer {
public var comments: [Comment]

init(comments: [Comment] = []) {
self.comments = comments
}

func parser(finding: Finding) {
var type : Kind
switch finding.severity {
case .error:
type = .essential
case .warning:
type = .actionable
default:
type = .informative
}

var comment: Comment

if let location = finding.location {
comment = Comment(comment: "swift.swift-linter.\(finding.severity)", params: ["message": finding.message.text, "line_number": String(location.line), "column_number": String(location.column)], type: type)
}
else {
comment = Comment(comment: "swift.swift-linter.\(finding.severity)", params: ["message": finding.message.text, ], type: type)
}
comments.append(comment)
}
}
32 changes: 32 additions & 0 deletions Sources/Analyzer/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import SwiftFormat

struct Main {
static func main(solutionFile: String, slug: String, analysisFile: String) {
let config = Configuration()
let analyzer = Analyzer()
let clousure = { (finding: Finding) in
analyzer.parser(finding: finding)
}

let Linter = SwiftLinter(configuration: config, findingConsumer: clousure)
let fileUrl = URL(fileURLWithPath: solutionFile)
try! Linter.lint(contentsOf: fileUrl)

let analysis = Analysis(comments: analyzer.comments)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.outputFormatting.update(with: .sortedKeys)
do {
let jsonData = try encoder.encode(analysis)
let fileURL = URL(fileURLWithPath: analysisFile)
try jsonData.write(to: fileURL)
} catch {
print("Failed to encode User: \(error)")
}
}
}



Main.main(solutionFile: CommandLine.arguments[1], slug: CommandLine.arguments[2], analysisFile: CommandLine.arguments[3])
18 changes: 18 additions & 0 deletions analysis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"comments" : [
{
"params" : {
"message" : "replace use of '.forEach { ... }' with for-in loop"
},
"type" : "actionable",
"comment" : "swift-linter"
},
{
"params" : {
"message" : "rename the constant 'Linter' using lowerCamelCase"
},
"comment" : "swift-linter",
"type" : "actionable"
}
]
}
Loading
Loading