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

Programming exercises: Add static code analysis for Python exercises with integrated code lifecycle #9573

Merged
merged 36 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
366aae7
Generate Sarif POJOs from JSON schema
magaupp Oct 23, 2024
79920e8
Add SARIF parser
magaupp Oct 23, 2024
52ef50d
Remove command field from StaticCodeAnalysisTool
magaupp Oct 23, 2024
516cb41
Remove language field from StaticCodeAnalysisTool
magaupp Oct 24, 2024
6935b9e
Add OTHER StaticCodeAnalysisTool
magaupp Oct 24, 2024
61af0eb
Rename XML variables
magaupp Oct 22, 2024
89290ad
Remove unused ReportParser parts
magaupp Oct 22, 2024
8521f2b
Add parser test
magaupp Oct 25, 2024
8442b1a
Statically initialize ParserPolicy
magaupp Oct 28, 2024
8882e50
Factor out methods in SarifParser
magaupp Oct 28, 2024
eb7b790
Document RuleCategorizer
magaupp Oct 28, 2024
56b4aed
Change pacakge of generated classes
magaupp Oct 30, 2024
39f57be
Merge branch 'develop' into feature/programming-exercises/sarif-parser
magaupp Oct 30, 2024
41a049b
Add Ruff Python SCA
magaupp Oct 23, 2024
46fda95
Add test
magaupp Oct 22, 2024
84f4be4
Merge branch 'develop' into feature/programming-exercises/sarif-parser
magaupp Nov 2, 2024
326ba06
Merge branch 'feature/programming-exercises/sarif-parser' into featur…
magaupp Nov 7, 2024
a44e3fd
Merge branch 'develop' into feature/programming-exercises/sarif-parser
magaupp Nov 10, 2024
1d248cf
Handle Result exceptions locally
magaupp Nov 10, 2024
2ed5974
Add more tests
magaupp Nov 10, 2024
5dfd8ff
Extract location processing method
magaupp Nov 10, 2024
d4508ae
Merge branch 'feature/programming-exercises/sarif-parser' into featur…
magaupp Nov 12, 2024
8f20007
Use explicit type definitions instead of schema
magaupp Nov 14, 2024
47c2671
Merge branch 'develop' into feature/programming-exercises/sarif-parser
magaupp Nov 14, 2024
a626123
Merge branch 'feature/programming-exercises/sarif-parser' into featur…
magaupp Nov 14, 2024
a021329
Merge branch 'develop' into feature/programming-exercises/sarif-parser
magaupp Nov 21, 2024
b10f892
Merge branch 'feature/programming-exercises/sarif-parser' into featur…
magaupp Nov 22, 2024
4b94be6
Merge branch 'develop' into feature/programming-exercises/python-sca
magaupp Dec 1, 2024
218e971
Update docker image version
magaupp Dec 1, 2024
cc0e9a3
Add lint selection in config file
magaupp Dec 1, 2024
68b69a3
Enable static code analysis specific files
magaupp Dec 5, 2024
7cc3748
Simplify test case
magaupp Dec 5, 2024
3bcb8a2
Add "Unknown" category
magaupp Dec 5, 2024
abce358
Merge branch 'develop' into feature/programming-exercises/python-sca
magaupp Dec 5, 2024
60d5164
Merge branch 'develop' into feature/programming-exercises/python-sca
magaupp Dec 8, 2024
7069a32
Merge branch 'develop' into feature/programming-exercises/python-sca
magaupp Dec 19, 2024
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
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();
}
magaupp marked this conversation as resolved.
Show resolved Hide resolved

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();
magaupp marked this conversation as resolved.
Show resolved Hide resolved
}
}
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}"
}
magaupp marked this conversation as resolved.
Show resolved Hide resolved

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
}
magaupp marked this conversation as resolved.
Show resolved Hide resolved

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
Loading