diff --git a/code-transformation/build.gradle b/code-transformation/build.gradle index 1bf1c5e4d..71620a21b 100644 --- a/code-transformation/build.gradle +++ b/code-transformation/build.gradle @@ -30,7 +30,8 @@ dependencies { // This dependency is used by the application. implementation 'com.google.guava:guava:31.0.1-jre' // implementation 'fr.inria.gforge.spoon:spoon-core:10.0.1-beta-2' - implementation 'org.eclipse.jgit:org.eclipse.jgit:+' + implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '6.0.0.202111291000-r' + implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit.ssh.apache', version: '6.0.0.202111291000-r' // implementation 'com.github.MartinWitt:Spoon:59fa98b3f8f83b2b566dc876ffabb26ba31ce2e3' // code solver branch implementation 'fr.inria.gforge.spoon:spoon-core:10.0.1-SNAPSHOT' implementation 'com.google.flogger:flogger:0.7.4' diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java index c0a139007..aa3c8e5b3 100644 --- a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java @@ -42,6 +42,7 @@ public class TransformationEngine { private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); private List>> processors; private IPrinting printing; + private ChangeListener changeListener; public TransformationEngine(List>> processors) { this.processors = processors; } @@ -54,6 +55,10 @@ public TransformationEngine() { PrimitiveToString::new); } + public void setChangeListener(ChangeListener changeListener) { + this.changeListener = changeListener; + } + public TransformationEngine setPrinting(IPrinting printing) { this.printing = printing; return this; @@ -69,16 +74,15 @@ public Changelog applyToGivenPath(String path) { printing = new ChangedTypePrinting(environment.createPrettyPrinter()); } PrinterCreation.setPrettyPrinter(environment, model); - ChangeListener listener = new ChangeListener(); - ProcessingManager pm = new RepeatingProcessingManager(launcher.getFactory(), listener); - addProcessors(pm, listener); - QodanaRefactor refactor = new QodanaRefactor(listener); - refactor.run(Path.of(path)); - pm.addProcessor(refactor); + if (changeListener == null) { + changeListener = new ChangeListener(); + } + ProcessingManager pm = new RepeatingProcessingManager(launcher.getFactory(), changeListener); + addProcessors(pm, changeListener); pm.process(model.getAllTypes()); Collection> newTypes = model.getAllTypes(); - printing.printChangedTypes(listener, newTypes); - return listener.getChangelog(); + printing.printChangedTypes(changeListener, newTypes); + return changeListener.getChangelog(); } protected void addInput(String path, Launcher launcher) { @@ -99,13 +103,15 @@ public Changelog applyToGivenPath(String path, String typeName) { if (printing == null) { printing = new ChangedTypePrinting(environment.createPrettyPrinter()); } - ChangeListener listener = new ChangeListener(); - ProcessingManager pm = new RepeatingProcessingManager(launcher.getFactory(), listener); + if (changeListener == null) { + changeListener = new ChangeListener(); + } + ProcessingManager pm = new RepeatingProcessingManager(launcher.getFactory(), changeListener); Collection> newTypes = getTypesWithName(typeName, model); - addProcessors(pm, listener); + addProcessors(pm, changeListener); pm.process(newTypes); - printing.printChangedTypes(listener, newTypes); - return listener.getChangelog(); + printing.printChangedTypes(changeListener, newTypes); + return changeListener.getChangelog(); } private static List> getTypesWithName(String typeName, CtModel model) { diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRefactor.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRefactor.java index 0650e6f16..68468d824 100644 --- a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRefactor.java +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRefactor.java @@ -32,6 +32,13 @@ public class QodanaRefactor extends TransformationProcessor> { private Map> ruleParser; private List refactorings; + private QodanaRefactor(Builder builder) { + super(builder.listener); + refactorings = new ArrayList<>(); + this.listener = builder.listener; + this.ruleParser = builder.ruleParser; + } + public QodanaRefactor(ChangeListener listener) { super(listener); this.listener = listener; @@ -50,7 +57,23 @@ public QodanaRefactor(ChangeListener listener) { * @param projectRoot The root of the project which should be analysed. */ public void run(Path projectRoot) { - List results = new QodanaRunner().runQodana(projectRoot); + QodanaRunner runner = new QodanaRunner.Builder().build(); + List results = runner.runQodana(projectRoot); + for (Result result : results) { + var parser = ruleParser.get(result.getRuleId()); + if (parser != null) { + refactorings.add(parser.apply(result)); + } + } + } + + /** + * Analyses the source code in the given source root + * @param projectRoot The root of the project which should be analysed. + */ + public void run(Path projectRoot, String srcPath) { + QodanaRunner runner = new QodanaRunner.Builder().withSourceFileRoot(srcPath).build(); + List results = runner.runQodana(projectRoot); for (Result result : results) { var parser = ruleParser.get(result.getRuleId()); if (parser != null) { @@ -65,4 +88,50 @@ public void process(CtType type) { refactoring.refactor(listener, type); } } + + public static class Builder { + + private ChangeListener listener; + private Map> ruleParser = new HashMap<>(); + public Builder(ChangeListener listener) { + this.listener = listener; + } + + public Builder withUnnecessaryReturn() { + ruleParser.put("UnnecessaryReturn", UnnecessaryReturn::new); + return this; + } + + public Builder withUnnecessaryToStringCall() { + ruleParser.put("UnnecessaryToStringCall", UnnecessaryToStringCall::new); + return this; + } + + public Builder withNonProtectedConstructorInAbstractClass() { + ruleParser.put("NonProtectedConstructorInAbstractClass", NonProtectedConstructorInAbstractClass::new); + return this; + } + + public Builder withUnnecessaryInterfaceModifier() { + ruleParser.put("UnnecessaryInterfaceModifier", UnnecessaryInterfaceModifier::new); + return this; + } + + public Builder withParameterNameDiffersFromOverriddenParameter() { + ruleParser.put("ParameterNameDiffersFromOverriddenParameter", + ParameterNameDiffersFromOverriddenParameter::new); + return this; + } + + public Builder withMethodMayBeStatic() { + ruleParser.put("MethodMayBeStatic", MethodMayBeStatic::new); + return this; + } + + public QodanaRefactor build() { + return new QodanaRefactor(this); + } + + } + } diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRunner.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRunner.java index b420691f6..29a6f91d5 100644 --- a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRunner.java +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/qodana/QodanaRunner.java @@ -38,25 +38,27 @@ class QodanaRunner { - private static final String RESULTS_PATH = "./.results/"; - private static final String CACHE_PATH = "./.laughing/"; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private String qodanaImageName = "jetbrains/qodana-jvm-community:2021.3"; - private String resultPathString = RESULTS_PATH + "qodana.sarif.json"; + private String resultFolder; + private String cacheFolder; + private String qodanaImageName; + private String resultPathString; + private boolean removeResultDir = true; + private String sourceFileRoot = "./src/main/java"; + + private QodanaRunner(Builder builder) { + this.resultFolder = builder.resultFolder; + this.cacheFolder = builder.cacheFolder; + this.qodanaImageName = builder.qodanaImageName; + this.resultPathString = builder.resultPathString; + this.removeResultDir = builder.removeResultDir; + this.sourceFileRoot = builder.sourceFileRoot; + } public List runQodana(Path sourceRoot) { + sourceRoot = fixWindowsPath(sourceRoot); logger.atInfo().log("Running Qodana on %s", sourceRoot); - try { - sourceRoot = fixWindowsPath(sourceRoot); - File qodanaRules = new File(this.getClass().getResource("/qodana.yml").toURI()); - File copyQodanaRules = new File(sourceRoot.toString(), "qodana.yaml"); - Files.writeString(copyQodanaRules.toPath(), Files.readString(qodanaRules.toPath()), - StandardOpenOption.CREATE); - - } - catch (URISyntaxException | IOException e1) { - logger.atSevere().withCause(e1).log("Could not write qodana.yaml"); - } + copyQodanaRules(sourceRoot); DockerClientConfig standard = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); DockerHttpClient httpClient = createHttpClient(standard); try (DockerClient dockerClient = DockerClientImpl.getInstance(standard, httpClient);) { @@ -66,14 +68,7 @@ public List runQodana(Path sourceRoot) { } Optional qodana = findQodanaImage(dockerClient); if (qodana.isPresent()) { - HostConfig hostConfig = createHostConfig(sourceRoot); - CreateContainerResponse container = createQodanaContainer(dockerClient, qodana, hostConfig); - List results = startQodanaContainer(dockerClient, container); - // cleanUpContainer(dockerClient, container); - FileUtils.deleteDirectory(Path.of(RESULTS_PATH).toFile()); - FileUtils.deleteDirectory(Path.of(CACHE_PATH).toFile()); - Files.deleteIfExists(Path.of(sourceRoot.toString(), "qodana.yaml")); - return results; + return executeQodana(sourceRoot, dockerClient, qodana); } } catch (Exception e) { @@ -82,8 +77,42 @@ public List runQodana(Path sourceRoot) { return List.of(); } - private void cleanUpContainer(DockerClient dockerClient, CreateContainerResponse container) { - dockerClient.removeContainerCmd(container.getId()).withRemoveVolumes(true).exec(); + private List executeQodana(Path sourceRoot, DockerClient dockerClient, Optional qodana) + throws InterruptedException, IOException { + HostConfig hostConfig = createHostConfig(sourceRoot); + CreateContainerResponse container = createQodanaContainer(dockerClient, qodana.get(), hostConfig); + List results = startQodanaContainer(dockerClient, container); + cleanCaches(sourceRoot); + return results; + } + + private void cleanCaches(Path sourceRoot) throws IOException { + if (removeResultDir) { + FileUtils.deleteDirectory(stringToFile(resultFolder)); + } + FileUtils.deleteDirectory(stringToFile(cacheFolder)); + Files.deleteIfExists(Path.of(sourceRoot.toString(), "qodana.yaml")); + } + + /** + * Converts the given path as string to a file object + * @param path the path as string + * @return the file object + */ + private File stringToFile(String path) { + return Path.of(path).toFile(); + } + + private void copyQodanaRules(Path sourceRoot) { + try { + File qodanaRules = new File(this.getClass().getResource("/qodana.yml").toURI()); + File copyQodanaRules = new File(sourceRoot.toString(), "qodana.yaml"); + Files.writeString(copyQodanaRules.toPath(), Files.readString(qodanaRules.toPath()), + StandardOpenOption.CREATE); + } + catch (URISyntaxException | IOException e) { + logger.atSevere().withCause(e).log("Could not write qodana.yaml"); + } } private List startQodanaContainer(DockerClient dockerClient, CreateContainerResponse container) @@ -109,13 +138,13 @@ public void onNext(WaitResponse object) { return results; } - private CreateContainerResponse createQodanaContainer(DockerClient dockerClient, Optional qodana, + private CreateContainerResponse createQodanaContainer(DockerClient dockerClient, Image qodana, HostConfig hostConfig) { - return dockerClient.createContainerCmd(qodana.get().getId()) + return dockerClient.createContainerCmd(qodana.getId()) .withHostConfig(hostConfig) .withAttachStderr(true) .withAttachStdout(true) - .withCmd("-d", "./src/main/java") + .withCmd("-d", sourceFileRoot) .exec(); } @@ -124,8 +153,8 @@ private HostConfig createHostConfig(Path sourceRoot) { Volume targetFile = new Volume("/data/results/"); Volume cacheDir = new Volume("/data/cache/"); Bind bind = new Bind(sourceRoot.toFile().getAbsolutePath(), sourceFile); - Bind resultsBind = new Bind(new File(RESULTS_PATH).getAbsolutePath(), targetFile); - Bind cacheBind = new Bind(new File(CACHE_PATH).getAbsolutePath(), cacheDir); + Bind resultsBind = new Bind(new File(resultFolder).getAbsolutePath(), targetFile); + Bind cacheBind = new Bind(new File(cacheFolder).getAbsolutePath(), cacheDir); return HostConfig.newHostConfig().withBinds(bind, cacheBind, resultsBind).withAutoRemove(true); } @@ -162,4 +191,45 @@ private List parseSarif(Path resultPath) throws IOException { SarifSchema210 sarif = mapper.readValue(reader, SarifSchema210.class); return sarif.getRuns().get(0).getResults(); } + + static class Builder { + + private String resultFolder = "./.results/"; + private String cacheFolder = "./.laughing/"; + private String qodanaImageName = "jetbrains/qodana-jvm-community:2021.3"; + private String resultPathString = resultFolder + "qodana.sarif.json"; + private boolean removeResultDir = true; + private String sourceFileRoot = "./src/main/java"; + + public Builder withResultFolder(String resultFolder) { + this.resultFolder = resultFolder; + this.resultPathString = resultFolder + "qodana.sarif.json"; + return this; + } + + public Builder withCacheFolder(String cacheFolder) { + this.cacheFolder = cacheFolder; + return this; + } + + public Builder withQodanaImageName(String qodanaImageName) { + this.qodanaImageName = qodanaImageName; + return this; + } + + public Builder withRemoveResultDir(boolean removeResultDir) { + this.removeResultDir = removeResultDir; + return this; + } + + public Builder withSourceFileRoot(String sourceFileRoot) { + this.sourceFileRoot = sourceFileRoot; + return this; + } + + public QodanaRunner build() { + return new QodanaRunner(this); + } + + } } diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/github/PullRequest.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/github/PullRequest.java new file mode 100644 index 000000000..417d0e9ba --- /dev/null +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/github/PullRequest.java @@ -0,0 +1,212 @@ + +package xyz.keksdose.spoon.code_solver.github; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.flogger.FluentLogger; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.ServiceUnavailableException; +import org.eclipse.jgit.api.errors.UnmergedPathsException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.sshd.SshdSessionFactory; +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder; +import org.eclipse.jgit.util.FS; + +import xyz.keksdose.spoon.code_solver.TransformationEngine; +import xyz.keksdose.spoon.code_solver.analyzer.qodana.QodanaRefactor; +import xyz.keksdose.spoon.code_solver.config.ConfigStore; +import xyz.keksdose.spoon.code_solver.history.Change; +import xyz.keksdose.spoon.code_solver.history.ChangeListener; +import xyz.keksdose.spoon.code_solver.history.Changelog; +import xyz.keksdose.spoon.code_solver.history.Link; +import xyz.keksdose.spoon.code_solver.history.MarkdownString; +import xyz.keksdose.spoon.code_solver.transformations.BadSmell; +import xyz.keksdose.spoon.code_solver.transformations.TransformationProcessor; + +public class PullRequest { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static void refactorRepoQodana(String repoName, String repoUrl, String gitHubRepoName, + String sourceDirectory) { + try { + ConfigStore config = new ConfigStore(); + File tempRepoFolder = Files.createTempDirectory(repoName).toFile(); + Git.cloneRepository().setURI(repoUrl).setDirectory(tempRepoFolder).call(); + try (Repository repository = Git.open(tempRepoFolder).checkout().getRepository()) { + Git git = new Git(repository); + switchToCleanBranch(config, git); + ChangeListener changeListener = new ChangeListener(); + Function> qodanaRefactorFunction = setUpQodana( + sourceDirectory, tempRepoFolder, changeListener); + Changelog changelog = refactorFolderWithQodana(tempRepoFolder, changeListener, qodanaRefactorFunction); + Optional change = getAnyChange(changelog); + if (change.isPresent()) { + addFileToGit(tempRepoFolder, git, change.get()); + createCommit(config, git, changelog); + printChangeLogMarkDown(config, changelog); + setUpSshSession(); + git.push().setRemote(gitHubRepoName).call(); + } + git.close(); + } + catch (IOException | GitAPIException e) { + logger.atSevere().withCause(e).log("Could not refactor repo"); + } + } + catch (IOException | GitAPIException e) { + logger.atSevere().withCause(e).log("Could not create temp directory"); + } + + } + + private static void setUpSshSession() { + File sshDir = new File(FS.DETECTED.userHome(), "/.ssh"); + SshdSessionFactory sshSessionFactory = new SshdSessionFactoryBuilder().setPreferredAuthentications("publickey") + .setHomeDirectory(FS.DETECTED.userHome()) + .setSshDirectory(sshDir) + .build(null); + SshSessionFactory.setInstance(sshSessionFactory); + } + + private static void printChangeLogMarkDown(ConfigStore config, Changelog changelog) { + if (config.getPrintMarkdown()) { + createMarkdown(changelog, Path.of(config.getMarkdownChangeLogFile())); + } + } + + private static void createCommit(ConfigStore config, Git git, Changelog changelog) + throws GitAPIException, AbortedByHookException, ConcurrentRefUpdateException, NoHeadException, + NoMessageException, ServiceUnavailableException, UnmergedPathsException, WrongRepositoryStateException { + git.commit() + .setAuthor(config.getGitAuthor(), config.getGitEmail()) + .setMessage("refactor: \n " + getFixedIssues(changelog)) + .call(); + } + + private static void addFileToGit(File tempRepoFolder, Git git, Change change) throws IOException, GitAPIException { + File changedFile = change.getAffectedType().getPosition().getFile(); + Path changedFilePath = Files.find(tempRepoFolder.toPath(), Integer.MAX_VALUE, + (path, attributes) -> path.getName(path.getNameCount() - 1).toString().equals(changedFile.getName()), + FileVisitOption.FOLLOW_LINKS).findFirst().get(); + String changedFileName = tempRepoFolder.toPath().relativize(changedFilePath).toString().replace("\\", "/"); + git.add().addFilepattern(changedFileName).call(); + } + + private static Optional getAnyChange(Changelog changelog) { + return changelog.getChanges().stream().findAny(); + } + + private static Changelog refactorFolderWithQodana(File tempRepoFolder, ChangeListener changeListener, + Function> qodanaRefactorFunction) { + TransformationEngine transformationEngine = new TransformationEngine(List.of(qodanaRefactorFunction)); + transformationEngine.setChangeListener(changeListener); + return transformationEngine.applyToGivenPath(tempRepoFolder.getAbsolutePath()); + } + + private static Function> setUpQodana(String sourceDirectory, + File tempRepoFolder, ChangeListener changeListener) { + QodanaRefactor qodanaRefactor = new QodanaRefactor.Builder(changeListener).withMethodMayBeStatic().build(); + qodanaRefactor.run(tempRepoFolder.getAbsoluteFile().toPath(), sourceDirectory); + return listener -> qodanaRefactor; + } + + private static void switchToCleanBranch(ConfigStore config, Git git) throws GitAPIException { + git.checkout().setName(config.getGitDefaultBranchName()).call(); + git.checkout() + .setForced(true) + .setCreateBranch(true) + .setName(config.getGitBranchPrefix() + LocalDateTime.now().getNano()) + .call(); + } + + private static String getFixedIssues(Changelog changelog) { + return changelog.getChanges() + .stream() + .map(Change::getBadSmell) + .filter(Objects::nonNull) + .map(BadSmell::getName) + .map(MarkdownString::asText) + .distinct() + .collect(Collectors.joining("\n")); + } + + private static void createMarkdown(Changelog changelog, Path path) { + Map> changesByType = changelog.getChanges() + .stream() + .collect(Collectors.groupingBy(Change::getIssue)); + StringBuilder sb = new StringBuilder(); + sb.append("# Change Log\n"); + appendBadSmells(changelog, sb); + sb.append("## The following has changed in the code:\n"); + appendChanges(changesByType, sb); + try { + Files.writeString(path, sb); + } + catch (IOException e) { + logger.atSevere().withCause(e).log("Could not write markdown changelog" + path); + } + } + + private static void appendChanges(Map> changesByType, StringBuilder sb) { + for (Entry> entry : changesByType.entrySet()) { + sb.append("### " + entry.getKey() + "\n"); + sb.append(entry.getValue() + .stream() + .map(c -> "- " + c.getChangeText().asMarkdown()) + .collect(Collectors.joining("\n"))); + sb.append("\n"); + } + } + + private static void appendBadSmells(Changelog changelog, StringBuilder sb) { + sb.append("The following bad smells are refactored:\n"); + List badSmells = changelog.getChanges() + .stream() + .map(Change::getBadSmell) + .filter(v -> !v.isEmptyRule()) + .distinct() + .sorted((o1, o2) -> o1.getName().asText().compareTo(o2.getName().asText())) + .collect(Collectors.toList()); + for (BadSmell badSmell : badSmells) { + sb.append("## " + badSmell.getName().asText() + "\n"); + sb.append(badSmell.getDescription().asMarkdown() + "\n"); + for (Link link : badSmell.getLinks()) { + sb.append("- " + link + "\n"); + } + + } + sb.append("\n"); + } + + private static String getRelevantChangeLog(String name, Changelog log) { + StringBuilder sb = new StringBuilder(); + sb.append("The following has changed in the code:\n"); + for (Change change : log.getChanges()) { + if (change.getAffectedType().getSimpleName().equals(name)) { + sb.append(change.getChangeText().asText() + "\n"); + } + } + return sb.toString(); + } +} diff --git a/doc/BadSmells.md b/doc/BadSmells.md index 6d97e3f3f..327ce402f 100644 --- a/doc/BadSmells.md +++ b/doc/BadSmells.md @@ -52,7 +52,7 @@ Primitive types have default values and setting them to the same value is redund Inner classes should be static if possible ## String-ValueOf-Primitive -Primitive types are converted to String using concationation with `""``String.valueOf(primitive)` is the preferred way to convert a primitive to a String. +Primitive types are converted to String using concationation with `""`. `String.valueOf(primitive)` is the preferred way to convert a primitive to a String. ## StringBuilderDirectUse `StringBuilder` offers a lot of methods directly and `toString` is not everytime needed diff --git a/doc/QodanaBadSmells.md b/doc/QodanaBadSmells.md index 22bc1ae8f..de031a6b9 100644 --- a/doc/QodanaBadSmells.md +++ b/doc/QodanaBadSmells.md @@ -1,4 +1,7 @@ # Qodana Rules +## Method-may-be-static +Method can be static. This increases the performance of the application. + ## Non-Protected-Constructor-in-Abstract-Class A non-protected constructor in an abstract class is not needed, because only subclasses can be instantiated