Skip to content

Commit

Permalink
Programming exercises: Add static code analysis for Python exercises …
Browse files Browse the repository at this point in the history
…with integrated code lifecycle (#9573)
  • Loading branch information
magaupp authored Dec 20, 2024
1 parent a73231a commit c46a670
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,74 @@
*/
public class StaticCodeAnalysisConfigurer {

// @formatter:off
private static final List<String> CATEGORY_NAMES_PYTHON = List.of(
"Pyflakes",
"pycodestyle",
"mccabe",
"isort",
"pep8-naming",
"pydocstyle",
"pyupgrade",
"flake8-2020",
"flake8-annotations",
"flake8-async",
"flake8-bandit",
"flake8-blind-except",
"flake8-boolean-trap",
"flake8-bugbear",
"flake8-builtins",
"flake8-commas",
"flake8-copyright",
"flake8-comprehensions",
"flake8-datetimez",
"flake8-debugger",
"flake8-django",
"flake8-errmsg",
"flake8-executable",
"flake8-future-annotations",
"flake8-implicit-str-concat",
"flake8-import-conventions",
"flake8-logging",
"flake8-logging-format",
"flake8-no-pep420",
"flake8-pie",
"flake8-print",
"flake8-pyi",
"flake8-pytest-style",
"flake8-quotes",
"flake8-raise",
"flake8-return",
"flake8-self",
"flake8-slots",
"flake8-simplify",
"flake8-tidy-imports",
"flake8-type-checking",
"flake8-gettext",
"flake8-unused-arguments",
"flake8-use-pathlib",
"flake8-todos",
"flake8-fixme",
"eradicate",
"pandas-vet",
"pygrep-hooks",
"Pylint",
"tryceratops",
"flynt",
"NumPy-specific rules",
"FastAPI",
"Airflow",
"Perflint",
"refurb",
"pydoclint",
"Ruff-specific rules",
"Unknown"
);
// @formatter:on

private static final Map<ProgrammingLanguage, List<StaticCodeAnalysisDefaultCategory>> languageToDefaultCategories = Map.of(ProgrammingLanguage.JAVA,
createDefaultCategoriesForJava(), ProgrammingLanguage.SWIFT, createDefaultCategoriesForSwift(), ProgrammingLanguage.C, createDefaultCategoriesForC());
createDefaultCategoriesForJava(), ProgrammingLanguage.SWIFT, createDefaultCategoriesForSwift(), ProgrammingLanguage.C, createDefaultCategoriesForC(),
ProgrammingLanguage.PYTHON, createDefaultCategoriesForPython());

/**
* Create an unmodifiable List of default static code analysis categories for Java
Expand Down Expand Up @@ -85,6 +151,11 @@ private static List<StaticCodeAnalysisDefaultCategory> createDefaultCategoriesFo
new StaticCodeAnalysisDefaultCategory("Miscellaneous", 0.2D, 2D, CategoryState.INACTIVE, List.of(createMapping(StaticCodeAnalysisTool.GCC, "Misc"))));
}

private static List<StaticCodeAnalysisDefaultCategory> createDefaultCategoriesForPython() {
return CATEGORY_NAMES_PYTHON.stream()
.map(name -> new StaticCodeAnalysisDefaultCategory(name, 0.0, 1.0, CategoryState.FEEDBACK, List.of(createMapping(StaticCodeAnalysisTool.RUFF, name)))).toList();
}

public static Map<ProgrammingLanguage, List<StaticCodeAnalysisDefaultCategory>> staticCodeAnalysisConfiguration() {
return languageToDefaultCategories;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum StaticCodeAnalysisTool {
PMD_CPD("cpd.xml"),
SWIFTLINT("swiftlint-result.xml"),
GCC("gcc.xml"),
RUFF("ruff.sarif"),
OTHER(null),
;
// @formatter:on
Expand All @@ -26,7 +27,8 @@ public enum StaticCodeAnalysisTool {
private static final Map<ProgrammingLanguage, List<StaticCodeAnalysisTool>> TOOLS_OF_PROGRAMMING_LANGUAGE = new EnumMap<>(Map.of(
ProgrammingLanguage.JAVA, List.of(SPOTBUGS, CHECKSTYLE, PMD, PMD_CPD),
ProgrammingLanguage.SWIFT, List.of(SWIFTLINT),
ProgrammingLanguage.C, List.of(GCC)
ProgrammingLanguage.C, List.of(GCC),
ProgrammingLanguage.PYTHON, List.of(RUFF)
));
// @formatter:on

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class ProgrammingExerciseRepositoryService {

private static final String TEST_DIR = "test";

private static final String STATIC_CODE_ANALYSIS_DIR = "staticCodeAnalysis";

private static final String POM_XML = "pom.xml";

private static final String BUILD_GRADLE = "build.gradle";
Expand Down Expand Up @@ -114,7 +116,8 @@ void setupExerciseTemplate(final ProgrammingExercise programmingExercise, final
setupRepositories(programmingExercise, exerciseCreator, exerciseResources, solutionResources, testResources);
}

private record RepositoryResources(Repository repository, Resource[] resources, Path prefix, Resource[] projectTypeResources, Path projectTypePrefix) {
private record RepositoryResources(Repository repository, Resource[] resources, Path prefix, Resource[] projectTypeResources, Path projectTypePrefix,
Resource[] staticCodeAnalysisResources, Path staticCodeAnalysisPrefix) {
}

/**
Expand All @@ -128,17 +131,17 @@ private record RepositoryResources(Repository repository, Resource[] resources,
private RepositoryResources getRepositoryResources(final ProgrammingExercise programmingExercise, final RepositoryType repositoryType) throws GitAPIException {
final String programmingLanguage = programmingExercise.getProgrammingLanguage().toString().toLowerCase(Locale.ROOT);
final ProjectType projectType = programmingExercise.getProjectType();
final Path projectTypeTemplateDir = getTemplateDirectoryForRepositoryType(repositoryType);
final Path repositoryTypeTemplateDir = getTemplateDirectoryForRepositoryType(repositoryType);

final VcsRepositoryUri repoUri = programmingExercise.getRepositoryURL(repositoryType);
final Repository repo = gitService.getOrCheckoutRepository(repoUri, true);

// Get path, files and prefix for the programming-language dependent files. They are copied first.
final Path generalTemplatePath = ProgrammingExerciseService.getProgrammingLanguageTemplatePath(programmingExercise.getProgrammingLanguage())
.resolve(projectTypeTemplateDir);
.resolve(repositoryTypeTemplateDir);
Resource[] resources = resourceLoaderService.getFileResources(generalTemplatePath);

Path prefix = Path.of(programmingLanguage).resolve(projectTypeTemplateDir);
Path prefix = Path.of(programmingLanguage).resolve(repositoryTypeTemplateDir);

Resource[] projectTypeResources = null;
Path projectTypePrefix = null;
Expand All @@ -149,8 +152,8 @@ private RepositoryResources getRepositoryResources(final ProgrammingExercise pro
projectType);
final String projectTypePath = projectType.name().toLowerCase();
final Path generalProjectTypePrefix = Path.of(programmingLanguage, projectTypePath);
final Path projectTypeSpecificPrefix = generalProjectTypePrefix.resolve(projectTypeTemplateDir);
final Path projectTypeTemplatePath = programmingLanguageProjectTypePath.resolve(projectTypeTemplateDir);
final Path projectTypeSpecificPrefix = generalProjectTypePrefix.resolve(repositoryTypeTemplateDir);
final Path projectTypeTemplatePath = programmingLanguageProjectTypePath.resolve(repositoryTypeTemplateDir);

final Resource[] projectTypeSpecificResources = resourceLoaderService.getFileResources(projectTypeTemplatePath);

Expand All @@ -165,7 +168,19 @@ private RepositoryResources getRepositoryResources(final ProgrammingExercise pro
}
}

return new RepositoryResources(repo, resources, prefix, projectTypeResources, projectTypePrefix);
Resource[] staticCodeAnalysisResources = null;
Path staticCodeAnalysisPrefix = null;

if (programmingExercise.isStaticCodeAnalysisEnabled()) {
Path programmingLanguageStaticCodeAnalysisPath = ProgrammingExerciseService.getProgrammingLanguageTemplatePath(programmingExercise.getProgrammingLanguage())
.resolve(STATIC_CODE_ANALYSIS_DIR);
final Path staticCodeAnalysisTemplatePath = programmingLanguageStaticCodeAnalysisPath.resolve(repositoryTypeTemplateDir);

staticCodeAnalysisResources = resourceLoaderService.getFileResources(staticCodeAnalysisTemplatePath);
staticCodeAnalysisPrefix = Path.of(programmingLanguage, STATIC_CODE_ANALYSIS_DIR).resolve(repositoryTypeTemplateDir);
}

return new RepositoryResources(repo, resources, prefix, projectTypeResources, projectTypePrefix, staticCodeAnalysisResources, staticCodeAnalysisPrefix);
}

private Path getTemplateDirectoryForRepositoryType(final RepositoryType repositoryType) {
Expand Down Expand Up @@ -316,10 +331,13 @@ private void setupTemplateAndPush(final RepositoryResources repositoryResources,
final Path repoLocalPath = getRepoAbsoluteLocalPath(repositoryResources.repository);

fileService.copyResources(repositoryResources.resources, repositoryResources.prefix, repoLocalPath, true);
// Also copy project type specific files AFTERWARDS (so that they might overwrite the default files)
// Also copy project type and static code analysis specific files AFTERWARDS (so that they might overwrite the default files)
if (repositoryResources.projectTypeResources != null) {
fileService.copyResources(repositoryResources.projectTypeResources, repositoryResources.projectTypePrefix, repoLocalPath, true);
}
if (repositoryResources.staticCodeAnalysisResources != null) {
fileService.copyResources(repositoryResources.staticCodeAnalysisResources, repositoryResources.staticCodeAnalysisPrefix, repoLocalPath, true);
}

replacePlaceholders(programmingExercise, repositoryResources.repository);
commitAndPushRepository(repositoryResources.repository, templateName + "-Template pushed by Artemis", true, user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public LocalCIProgrammingLanguageFeatureService() {
programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, true));
programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, false, false, true, true, false, List.of(), false, true));
programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), false, true));
programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true));
programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, true, true, false, false, List.of(), false, true));
programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, true));
programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, true));
programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisTool;
import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.UnsupportedToolException;
import de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif.RuffCategorizer;
import de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif.SarifParser;

/**
* Policy class for the parser strategies.
Expand All @@ -27,7 +29,7 @@ public ParserStrategy configure(String fileName) {
case CHECKSTYLE -> new CheckstyleParser();
case PMD -> new PMDParser();
case PMD_CPD -> new PMDCPDParser();
// so far, we do not support swiftlint and gcc only SCA for Java
case RUFF -> new SarifParser(StaticCodeAnalysisTool.RUFF, new RuffCategorizer());
default -> throw new UnsupportedToolException("Tool " + tool + " is not supported");
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif;

import java.util.Map;

import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.PropertyBag;
import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor;

public class RuffCategorizer implements RuleCategorizer {

@Override
public String categorizeRule(ReportingDescriptor rule) {
Map<String, Object> properties = rule.getOptionalProperties().map(PropertyBag::additionalProperties).orElseGet(Map::of);
return properties.getOrDefault("kind", "Unknown").toString();
}
}
2 changes: 1 addition & 1 deletion src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ artemis:
empty:
default: "ubuntu:24.04"
python:
default: "ls1tum/artemis-python-docker:v1.0.0"
default: "ls1tum/artemis-python-docker:v1.1.0"
c:
# possible overrides: gcc, fact
default: "ls1tum/artemis-c-docker:v1.0.0"
Expand Down
30 changes: 30 additions & 0 deletions src/main/resources/templates/aeolus/python/default_static.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -e
export AEOLUS_INITIAL_DIRECTORY=${PWD}
static_code_analysis () {
echo '⚙️ executing static_code_analysis'
ruff check --config=ruff-student.toml --output-format=sarif --output-file=ruff.sarif --exit-zero "${studentParentWorkingDirectoryName}"
}

build_and_test_the_code () {
echo '⚙️ executing build_and_test_the_code'
python3 -m compileall . -q || error=true
if [ ! $error ]
then
pytest --junitxml=test-reports/results.xml
fi
}

main () {
if [[ "${1}" == "aeolus_sourcing" ]]; then
return 0 # just source to use the methods in the subshell, no execution
fi
local _script_name
_script_name=${BASH_SOURCE[0]:-$0}
cd "${AEOLUS_INITIAL_DIRECTORY}"
bash -c "source ${_script_name} aeolus_sourcing; static_code_analysis"
cd "${AEOLUS_INITIAL_DIRECTORY}"
bash -c "source ${_script_name} aeolus_sourcing; build_and_test_the_code"
}

main "${@}"
21 changes: 21 additions & 0 deletions src/main/resources/templates/aeolus/python/default_static.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
api: v0.0.1
actions:
- name: static_code_analysis
script: ruff check --config=ruff-student.toml --output-format=sarif --output-file=ruff.sarif --exit-zero "${studentParentWorkingDirectoryName}"
results:
- name: ruff
path: ruff.sarif
type: sca
- name: build_and_test_the_code
script: |-
python3 -m compileall . -q || error=true
if [ ! $error ]
then
pytest --junitxml=test-reports/results.xml
fi
runAlways: false
results:
- name: junit_test-reports/*results.xml
path: test-reports/*results.xml
type: junit
before: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[lint]
select = [
# Pyflakes
"F",
# pycodestyle
"E", "W",
# isort
"I",
# flake8-async
"ASYNC",
# flake8-bugbear
"B",
# flake8-comprehensions
"C4",
# flake8-pie
"PIE",
# flake8-return
"RET",
# flake8-self
"SLF",
# flake8-simplify
"SIM",
# flake8-unused-arguments
"ARG",
# pandas-vet
"PD",
# Pylint
"PL",
# NumPy-specific rules
"NPY",
# refurb
"FURB",
# Ruff-specific rules
"RUF",
]
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ void testSpotbugsParser() throws IOException {
testParserWithFile("spotbugsXml.xml", "spotbugs.txt");
}

@Test
void testRuffParser() throws IOException {
testParserWithFile("ruff.sarif", "ruff.json");
}

@Test
void testParseInvalidXML() {
assertThatCode(() -> testParserWithFileNamed("invalid_xml.xml", "pmd.xml", "invalid_xml.txt")).isInstanceOf(RuntimeException.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ private static StaticCodeAnalysisIssue generateStaticCodeAnalysisIssue(StaticCod
case PMD_CPD -> "Copy/Paste Detection";
case SWIFTLINT -> "swiftLint"; // TODO: rene: set better value after categories are better defined
case GCC -> "Memory";
case RUFF -> "Pylint";
case OTHER -> "Other";
};

Expand Down
Loading

0 comments on commit c46a670

Please sign in to comment.