au.com.integradev.delphi.checks.verifier.CheckVerifierImpl Maven / Gradle / Ivy
The newest version!
/*
* Sonar Delphi Plugin
* Copyright (C) 2023 Integrated Application Development
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package au.com.integradev.delphi.checks.verifier;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import au.com.integradev.delphi.DelphiProperties;
import au.com.integradev.delphi.antlr.DelphiFileStream;
import au.com.integradev.delphi.antlr.ast.visitors.SymbolAssociationVisitor;
import au.com.integradev.delphi.builders.DelphiTestFile;
import au.com.integradev.delphi.builders.DelphiTestUnitBuilder;
import au.com.integradev.delphi.check.MasterCheckRegistrar;
import au.com.integradev.delphi.compiler.CompilerVersion;
import au.com.integradev.delphi.compiler.Platform;
import au.com.integradev.delphi.compiler.Toolchain;
import au.com.integradev.delphi.file.DelphiFile;
import au.com.integradev.delphi.file.DelphiFile.DelphiInputFile;
import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory;
import au.com.integradev.delphi.preprocessor.directive.CompilerDirectiveParserImpl;
import au.com.integradev.delphi.preprocessor.search.SearchPath;
import au.com.integradev.delphi.reporting.TextRangeReplacement;
import au.com.integradev.delphi.reporting.edits.QuickFixEditImpl;
import au.com.integradev.delphi.symbol.SymbolTable;
import au.com.integradev.delphi.type.factory.TypeFactoryImpl;
import au.com.integradev.delphi.utils.DelphiUtils;
import com.google.common.base.Splitter;
import com.google.common.base.Suppliers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.TextRange;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.IssueLocation;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleScope;
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
import org.sonar.plugins.communitydelphi.api.reporting.QuickFix;
import org.sonar.plugins.communitydelphi.api.reporting.QuickFixEdit;
/**
* Based loosely on {@code InternalCheckVerifier} from the sonar-java project.
*
* @see
* InternalCheckVerifier
*/
public class CheckVerifierImpl implements CheckVerifier {
private static final Logger LOG = LoggerFactory.getLogger(CheckVerifierImpl.class);
private DelphiCheck check;
private DelphiTestFile testFile;
private CompilerVersion compilerVersion = DelphiProperties.COMPILER_VERSION_DEFAULT;
private Toolchain toolchain = DelphiProperties.COMPILER_TOOLCHAIN_DEFAULT;
private final Set unitScopeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
private final Map unitAliases = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private final List searchPathUnits = new ArrayList<>();
private final List standardLibraryUnits = new ArrayList<>();
@Override
public CheckVerifier withCheck(DelphiCheck check) {
requireUnassigned(this.check, "check");
this.check = check;
return this;
}
@Override
public CheckVerifier withCompilerVersion(CompilerVersion compilerVersion) {
this.compilerVersion = compilerVersion;
return this;
}
@Override
public CheckVerifier withToolchain(Toolchain toolchain) {
this.toolchain = toolchain;
return this;
}
@Override
public CheckVerifier withUnitScopeName(String unitScopeName) {
unitScopeNames.add(unitScopeName);
return this;
}
@Override
public CheckVerifier withUnitAlias(String alias, String unitName) {
unitAliases.put(alias, unitName);
return this;
}
@Override
public CheckVerifier withSearchPathUnit(DelphiTestUnitBuilder builder) {
searchPathUnits.add(builder);
return this;
}
@Override
public CheckVerifier withStandardLibraryUnit(DelphiTestUnitBuilder builder) {
standardLibraryUnits.add(builder);
return this;
}
@Override
public CheckVerifier onFile(DelphiTestFile builder) {
requireUnassigned(this.testFile, "file");
this.testFile = builder;
return this;
}
@Override
public void verifyIssues() {
ExecutionResult result = execute();
List issues = result.getIssues();
List quickFixes = result.getQuickFixes();
if (issues.isEmpty()) {
throw new AssertionError("No issue raised. At least one issue expected");
}
Expectations expected = Expectations.fromComments(testFile.delphiFile());
verifyIssuesOnLinesInternal(issues, expected.issues());
if (!expected.quickFixes().isEmpty()) {
assertQuickFixes(quickFixes, expected.quickFixes());
}
}
private static void verifyIssuesOnLinesInternal(
List issues, List expectedIssues) {
List unexpectedLines = new ArrayList<>();
List expectedLines =
expectedIssues.stream().map(IssueExpectation::getBeginLine).collect(Collectors.toList());
for (Issue issue : issues) {
IssueLocation issueLocation = issue.primaryLocation();
TextRange textRange = issueLocation.textRange();
if (textRange == null) {
throw new AssertionError(
String.format(
"Expected issues to be raised at line level, not at %s level",
issueLocation.inputComponent().isFile() ? "file" : "project"));
}
Integer line = textRange.start().line();
if (!expectedLines.remove(line)) {
unexpectedLines.add(line);
}
}
assertIssueMismatchesEmpty(expectedLines, unexpectedLines);
}
private void assertQuickFixes(
List actualQuickFixes, List expectedQuickFixes) {
if (expectedQuickFixes.size() != actualQuickFixes.size()) {
throw new AssertionError(
String.format(
"%d quick fixes expected, found %d",
expectedQuickFixes.size(), actualQuickFixes.size()));
}
List unmatchedActuals = new ArrayList<>(actualQuickFixes);
for (QuickFixExpectation expected : expectedQuickFixes) {
Optional matchingQuickFix =
unmatchedActuals.stream()
.filter(actual -> textEditsMatch(actual.getEdits(), expected.getExpectedTextEdits()))
.findFirst();
matchingQuickFix.ifPresent(unmatchedActuals::remove);
if (matchingQuickFix.isPresent()) {
unmatchedActuals.remove(matchingQuickFix.get());
} else {
throw new AssertionError(
String.format(
"Expected:%n%s%nFound %d non-matching quick fixes:%n%s",
getQuickFixString(expected),
actualQuickFixes.size(),
actualQuickFixes.stream()
.map(this::getQuickFixString)
.collect(Collectors.joining("\n"))));
}
}
if (!unmatchedActuals.isEmpty()) {
throw new AssertionError(
String.format(
"Found %d unexpected quick fixes:%n%s",
unmatchedActuals.size(),
unmatchedActuals.stream()
.map(this::getQuickFixString)
.collect(Collectors.joining("\n"))));
}
}
private String getQuickFixString(QuickFix quickFix) {
Supplier fileStreamSupplier = newFileStreamSupplier(testFile.delphiFile());
return "Quick fix:\n "
+ quickFix.getEdits().stream()
.map(QuickFixEditImpl.class::cast)
.map(e -> e.toTextEdits(fileStreamSupplier))
.flatMap(Collection::stream)
.map(CheckVerifierImpl::getTextEditString)
.collect(Collectors.joining("\n "));
}
private static String getQuickFixString(QuickFixExpectation quickFix) {
return "Quick fix "
+ quickFix.getFixId()
+ ":\n "
+ quickFix.getExpectedTextEdits().stream()
.map(CheckVerifierImpl::getTextEditString)
.collect(Collectors.joining("\n "));
}
private static String getTextEditString(TextRangeReplacement textEdit) {
return String.format(
"[%s:%s to %s:%s] replaced with %s",
textEdit.getLocation().getBeginLine(),
textEdit.getLocation().getBeginColumn(),
textEdit.getLocation().getEndLine(),
textEdit.getLocation().getEndColumn(),
textEdit.getReplacement());
}
private static String getTextEditString(TextEditExpectation textEdit) {
return String.format(
"[%s:%s to %s:%s] replaced with %s",
textEdit.getBeginLine(),
textEdit.getBeginColumn(),
textEdit.getEndLine(),
textEdit.getEndColumn(),
textEdit.getReplacement());
}
private static Supplier newFileStreamSupplier(DelphiFile delphiFile) {
return Suppliers.memoize(
() -> {
try {
return new DelphiFileStream(
delphiFile.getSourceCodeFile().getAbsolutePath(),
delphiFile.getSourceCodeFileEncoding());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
private boolean textEditsMatch(
List textEdits, List expectedTextEdits) {
Supplier fileStreamSupplier = newFileStreamSupplier(testFile.delphiFile());
List unmatchedActuals =
textEdits.stream()
.map(QuickFixEditImpl.class::cast)
.map(e -> e.toTextEdits(fileStreamSupplier))
.flatMap(Collection::stream)
.collect(Collectors.toCollection(ArrayList::new));
if (expectedTextEdits.size() != unmatchedActuals.size()) {
return false;
}
for (TextEditExpectation expected : expectedTextEdits) {
Optional matchingTextEdit =
unmatchedActuals.stream().filter(actual -> textEditMatches(actual, expected)).findFirst();
if (matchingTextEdit.isPresent()) {
unmatchedActuals.remove(matchingTextEdit.get());
} else {
return false;
}
}
return true;
}
private static boolean textEditMatches(
TextRangeReplacement actual, TextEditExpectation expected) {
boolean rangeMatches =
actual.getLocation().getBeginLine() == expected.getBeginLine()
&& actual.getLocation().getBeginColumn() == expected.getBeginColumn()
&& actual.getLocation().getEndLine() == expected.getEndLine()
&& actual.getLocation().getEndColumn() == expected.getEndColumn();
return rangeMatches && actual.getReplacement().equals(expected.getReplacement());
}
private static void assertIssueMismatchesEmpty(
List expectedLines, List unexpectedLines) {
if (!expectedLines.isEmpty() || !unexpectedLines.isEmpty()) {
StringBuilder message = new StringBuilder("Issues were ");
if (!expectedLines.isEmpty()) {
message.append("expected at ").append(expectedLines);
}
if (!expectedLines.isEmpty() && !unexpectedLines.isEmpty()) {
message.append(", ");
}
if (!unexpectedLines.isEmpty()) {
message.append("unexpected at ").append(unexpectedLines);
}
throw new AssertionError(message.toString());
}
}
@Override
public void verifyIssueOnFile() {
ExecutionResult result = execute();
IssueLocation issueLocation = verifySingleIssueOnComponent(result.getIssues(), "file");
if (!issueLocation.inputComponent().isFile()) {
throw new AssertionError(
"Expected the issue to be raised at file level, not at project level");
}
}
@Override
public void verifyIssueOnProject() {
ExecutionResult result = execute();
IssueLocation issueLocation = verifySingleIssueOnComponent(result.getIssues(), "project");
if (issueLocation.inputComponent().isFile()) {
throw new AssertionError(
"Expected the issue to be raised at project level, not at file level");
}
}
@Override
public void verifyNoIssues() {
ExecutionResult result = execute();
List issues = result.getIssues();
if (!issues.isEmpty()) {
throw new AssertionError(
String.format(
"No issues expected but got %d issues:%s", issues.size(), issuesToString(issues)));
}
}
private ExecutionResult execute() {
requireAssigned(check, "check");
requireAssigned(testFile, "file");
List lines = Splitter.on('\n').splitToList(testFile.sourceCode());
for (int lineNum = 0; lineNum < lines.size(); ++lineNum) {
String printLine = String.format("%03d %s", lineNum + 1, lines.get(lineNum));
LOG.info(printLine);
}
Path standardLibraryPath = createStandardLibrary();
DelphiInputFile file = testFile.delphiFile();
SymbolTable symbolTable =
SymbolTable.builder()
.preprocessorFactory(new DelphiPreprocessorFactory(Platform.WINDOWS))
.typeFactory(new TypeFactoryImpl(toolchain, compilerVersion))
.standardLibraryPath(standardLibraryPath)
.sourceFiles(List.of(file.getSourceCodeFile().toPath()))
.unitAliases(unitAliases)
.unitScopeNames(unitScopeNames)
.searchPath(createSearchPath())
.build();
FileUtils.deleteQuietly(standardLibraryPath.toFile());
new SymbolAssociationVisitor()
.visit(file.getAst(), new SymbolAssociationVisitor.Data(symbolTable));
var sensorContext = SensorContextTester.create(FileUtils.getTempDirectory());
sensorContext.settings().setProperty(DelphiProperties.TEST_TYPE_KEY, "Test.TTestSuite");
var compilerDirectiveParser =
new CompilerDirectiveParserImpl(
Platform.WINDOWS, file.getTextBlockLineEndingModeRegistry());
var checkRegistrar = mock(MasterCheckRegistrar.class);
when(checkRegistrar.getRuleKey(check))
.thenReturn(Optional.of(RuleKey.of("test", check.getClass().getSimpleName())));
when(checkRegistrar.getScope(check)).thenReturn(RuleScope.ALL);
var context =
new DelphiCheckContextTester(
check, sensorContext, file, compilerDirectiveParser, checkRegistrar);
check.start(context);
check.visit(file.getAst(), context);
check.end(context);
return new ExecutionResult(List.copyOf(sensorContext.allIssues()), context.getQuickFixes());
}
private static IssueLocation verifySingleIssueOnComponent(List issues, String component) {
if (issues.size() != 1) {
throw new AssertionError(
"A single issue is expected on the "
+ component
+ ", but "
+ (issues.isEmpty()
? "none has been raised."
: String.format("%d issues have been raised.", issues.size())));
}
IssueLocation issueLocation = issues.get(0).primaryLocation();
TextRange textRange = issueLocation.textRange();
if (textRange != null) {
throw new AssertionError(
String.format(
"Expected an issue directly on %s but was raised on line %d",
component, textRange.start().line()));
}
return issueLocation;
}
private static void requireUnassigned(@Nullable Object object, String fieldName) {
if (object != null) {
throw new AssertionError(String.format("Do not set %s multiple times!", fieldName));
}
}
private static void requireAssigned(@Nullable Object object, String fieldName) {
if (object == null) {
throw new AssertionError(
String.format("Set %s before calling any verification method!", fieldName));
}
}
private static String issuesToString(List issues) {
if (issues.isEmpty()) {
return "";
}
return issues.stream()
.map(Issue::primaryLocation)
.sorted(CheckVerifierImpl::compareIssueLocations)
.map(CheckVerifierImpl::issueLocationToString)
.collect(Collectors.joining("\n--> ", "\n--> ", ""));
}
private static int compareIssueLocations(IssueLocation l1, IssueLocation l2) {
if (l1.textRange() == null) {
return 1;
}
if (l2.textRange() == null) {
return -1;
}
return Integer.compare(
Objects.requireNonNull(l1.textRange()).start().line(),
Objects.requireNonNull(l2.textRange()).start().line());
}
private static String issueLocationToString(IssueLocation location) {
TextRange textRange = location.textRange();
return String.format(
"'%s' in %s%s",
location.message(),
location.inputComponent(),
(textRange == null ? "" : (":" + textRange.start().line())));
}
private Path createStandardLibrary() {
try {
Path path = Files.createTempDirectory("bds_source");
Files.writeString(
path.resolve("SysInit.pas"), "unit SysInit;\ninterface\nimplementation\nend.");
Files.writeString(
path.resolve("System.pas"),
"unit System;\n"
+ "\n"
+ "interface\n"
+ "\n"
+ "type\n"
+ " TArray = array of T;\n"
+ "\n"
+ " TObject = class;\n"
+ "\n"
+ " TClass = class of TObject;\n"
+ "\n"
+ " PMethod = ^TMethod;\n"
+ " TMethod = record\n"
+ " Code, Data: Pointer;\n"
+ " public\n"
+ " class operator Equal(const Left, Right: TMethod): Boolean;\n"
+ " class operator NotEqual(const Left, Right: TMethod): Boolean;\n"
+ " class operator GreaterThan(const Left, Right: TMethod): Boolean;\n"
+ " class operator GreaterThanOrEqual(const Left, Right: TMethod): Boolean;\n"
+ " class operator LessThan(const Left, Right: TMethod): Boolean;\n"
+ " class operator LessThanOrEqual(const Left, Right: TMethod): Boolean;\n"
+ " end;\n"
+ "\n"
+ " TObject = class\n"
+ " public\n"
+ " constructor Create;\n"
+ " procedure Free;\n"
+ " class function InitInstance(Instance: Pointer): TObject;\n"
+ " procedure CleanupInstance;\n"
+ " function ClassType: TClass; inline;\n"
+ " class function ClassName: string;\n"
+ " class function ClassNameIs(const Name: string): Boolean;\n"
+ " class function ClassParent: TClass;\n"
+ " class function ClassInfo: Pointer; inline;\n"
+ " class function InstanceSize: Longint; inline;\n"
+ " class function InheritsFrom(AClass: TClass): Boolean;\n"
+ " class function MethodAddress(const Name: _ShortStr): Pointer; overload;\n"
+ " class function MethodAddress(const Name: string): Pointer; overload;\n"
+ " class function MethodName(Address: Pointer): string;\n"
+ " class function QualifiedClassName: string;\n"
+ " function FieldAddress(const Name: _ShortStr): Pointer; overload;\n"
+ " function FieldAddress(const Name: string): Pointer; overload;\n"
+ " function GetInterface(const IID: TGUID; out Obj): Boolean;\n"
+ " class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;\n"
+ " class function GetInterfaceTable: PInterfaceTable;\n"
+ " class function UnitName: string;\n"
+ " class function UnitScope: string;\n"
+ " function Equals(Obj: TObject): Boolean; virtual;\n"
+ " function GetHashCode: Integer; virtual;\n"
+ " function ToString: string; virtual;\n"
+ " function SafeCallException(ExceptObject: TObject;\n"
+ " ExceptAddr: Pointer): HResult; virtual;\n"
+ " procedure AfterConstruction; virtual;\n"
+ " procedure BeforeDestruction; virtual;\n"
+ " procedure Dispatch(var Message); virtual;\n"
+ " procedure DefaultHandler(var Message); virtual;\n"
+ " class function NewInstance: TObject; virtual;\n"
+ " procedure FreeInstance; virtual;\n"
+ " destructor Destroy; virtual;\n"
+ " end;\n"
+ "\n"
+ " IInterface = interface\n"
+ " function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;\n"
+ " function _AddRef: Integer; stdcall;\n"
+ " function _Release: Integer; stdcall;\n"
+ " end;\n"
+ "\n"
+ " IEnumerator = interface(IInterface)\n"
+ " function GetCurrent: TObject;\n"
+ " function MoveNext: Boolean;\n"
+ " procedure Reset;\n"
+ " property Current: TObject read GetCurrent;\n"
+ " end;\n"
+ "\n"
+ " IEnumerable = interface(IInterface)\n"
+ " function GetEnumerator: IEnumerator;\n"
+ " end;\n"
+ "\n"
+ " IEnumerator = interface(IEnumerator)\n"
+ " function GetCurrent: T;\n"
+ " property Current: T read GetCurrent;\n"
+ " end;\n"
+ "\n"
+ " IEnumerable = interface(IEnumerable)\n"
+ " function GetEnumerator: IEnumerator;\n"
+ " end;\n"
+ "\n"
+ " IComparable = interface(IInterface)\n"
+ " function CompareTo(Obj: TObject): Integer;\n"
+ " end;\n"
+ "\n"
+ " IComparable = interface(IComparable)\n"
+ " function CompareTo(Value: T): Integer;\n"
+ " end;\n"
+ "\n"
+ " IEquatable = interface(IInterface)\n"
+ " function Equals(Value: T): Boolean;\n"
+ " end;\n"
+ "\n"
+ " PVarRec = ^TVarRec;\n"
+ " TVarRec = record\n"
+ " case Integer of\n"
+ " 0: (case Byte of\n"
+ " vtInteger: (VInteger: Integer);\n"
+ " vtBoolean: (VBoolean: Boolean);\n"
+ " vtChar: (VChar: _AnsiChr);\n"
+ " vtExtended: (VExtended: PExtended);\n"
+ " vtString: (VString: _PShortStr);\n"
+ " vtPointer: (VPointer: Pointer);\n"
+ " vtPChar: (VPChar: _PAnsiChr);\n"
+ " vtObject: (VObject: TObject);\n"
+ " vtClass: (VClass: TClass);\n"
+ " vtWideChar: (VWideChar: WideChar);\n"
+ " vtPWideChar: (VPWideChar: PWideChar);\n"
+ " vtAnsiString: (VAnsiString: Pointer);\n"
+ " vtCurrency: (VCurrency: PCurrency);\n"
+ " vtVariant: (VVariant: PVariant);\n"
+ " vtInterface: (VInterface: Pointer);\n"
+ " vtWideString: (VWideString: Pointer);\n"
+ " vtInt64: (VInt64: PInt64);\n"
+ " vtUnicodeString: (VUnicodeString: Pointer);\n"
+ " );\n"
+ " 1: (_Reserved1: NativeInt;\n"
+ " VType: Byte;\n"
+ " );\n"
+ " end;\n"
+ "\n"
+ " TInterfacedObject = class(TObject, IInterface)\n"
+ " protected\n"
+ " FRefCount: Integer;\n"
+ " function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;\n"
+ " function _AddRef: Integer; stdcall;\n"
+ " function _Release: Integer; stdcall;\n"
+ " public\n"
+ " procedure AfterConstruction; override;\n"
+ " procedure BeforeDestruction; override;\n"
+ " class function NewInstance: TObject; override;\n"
+ " property RefCount: Integer read FRefCount;\n"
+ " end;\n"
+ "\n"
+ " TInterfacedClass = class of TInterfacedObject;\n"
+ "\n"
+ " TClassHelperBase = class(TInterfacedObject, IInterface)\n"
+ " protected\n"
+ " FInstance: TObject;\n"
+ " constructor _Create(Instance: TObject);\n"
+ " end;\n"
+ "\n"
+ " TClassHelperBaseClass = class of TClassHelperBase;\n"
+ "\n"
+ " TCustomAttribute = class(TObject)\n"
+ " end;\n"
+ "\n"
+ "implementation\n"
+ "\n"
+ "end.");
Files.writeString(
path.resolve("System.Classes.pas"),
"unit System.Classes;\n"
+ "\n"
+ "interface\n"
+ "\n"
+ "type\n"
+ " TPersistent = class(TObject)\n"
+ "\n"
+ " end;\n"
+ "\n"
+ " TComponentClass = class of TComponent;\n"
+ "\n"
+ " TComponent = class(TPersistent, IInterface)\n"
+ " \n"
+ " end;\n"
+ "\n"
+ "implementation\n"
+ "\n"
+ "end.");
for (DelphiTestUnitBuilder unit : standardLibraryUnits) {
Path source = unit.delphiFile().getSourceCodeFile().toPath();
Path target = path.resolve(source.getFileName().toString());
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
return path;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private SearchPath createSearchPath() {
return SearchPath.create(
searchPathUnits.stream()
.map(builder -> builder.delphiFile().getInputFile())
.map(DelphiUtils::inputFileToPath)
.map(Path::getParent)
.collect(Collectors.toUnmodifiableList()));
}
private static class ExecutionResult {
private final List issues;
private final List quickFixes;
public ExecutionResult(List issues, List quickFixes) {
this.issues = issues;
this.quickFixes = quickFixes;
}
public List getIssues() {
return issues;
}
public List getQuickFixes() {
return quickFixes;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy