basic for analyse
This commit is contained in:
parent
41f2c804a0
commit
85a224fc7b
@ -57,7 +57,7 @@ public class BuilderPipelineService {
|
||||
public AnalysisSnapshot analyze(
|
||||
final BuilderPipelineContext ctx,
|
||||
final LogAggregator logs) {
|
||||
final var diagnostics = run(ctx, logs, analyses, new ArrayList<>());
|
||||
final var diagnostics = run(ctx, logs, analyses, new ArrayList<>(), false);
|
||||
return new AnalysisSnapshot(
|
||||
ctx.resolvedWorkspace.frontendSpec(),
|
||||
diagnostics,
|
||||
@ -70,7 +70,10 @@ public class BuilderPipelineService {
|
||||
final BuilderPipelineContext ctx,
|
||||
final LogAggregator logs) {
|
||||
final var analysisSnapshot = this.analyze(ctx, logs);
|
||||
final var diagnostics = run(ctx, logs, compile, new ArrayList<>(analysisSnapshot.diagnostics()));
|
||||
if (hasErrors(analysisSnapshot.diagnostics())) {
|
||||
throw new BuildException("issues found on pipeline stage: analyze");
|
||||
}
|
||||
final var diagnostics = run(ctx, logs, compile, new ArrayList<>(analysisSnapshot.diagnostics()), true);
|
||||
return new CompileResult(
|
||||
analysisSnapshot,
|
||||
diagnostics,
|
||||
@ -84,7 +87,7 @@ public class BuilderPipelineService {
|
||||
final BuilderPipelineContext ctx,
|
||||
final LogAggregator logs) {
|
||||
final var compileResult = this.compile(ctx, logs);
|
||||
final var diagnostics = run(ctx, logs, build, new ArrayList<>(compileResult.diagnostics()));
|
||||
final var diagnostics = run(ctx, logs, build, new ArrayList<>(compileResult.diagnostics()), true);
|
||||
return new BuildResult(
|
||||
compileResult,
|
||||
diagnostics,
|
||||
@ -95,19 +98,32 @@ public class BuilderPipelineService {
|
||||
final BuilderPipelineContext ctx,
|
||||
final LogAggregator logs,
|
||||
final List<PipelineStage> stages,
|
||||
final List<BuildingIssue> diagnostics) {
|
||||
final List<BuildingIssue> diagnostics,
|
||||
final boolean failFast) {
|
||||
for (final var builderPipelineStage : stages) {
|
||||
final var issues = builderPipelineStage.run(ctx, logs);
|
||||
diagnostics.addAll(issues.asCollection());
|
||||
printIssues(issues, logs);
|
||||
if (issues.hasErrors()) {
|
||||
throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName());
|
||||
if (failFast) {
|
||||
throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
logs.using(log).info("builder pipeline completed successfully");
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
private boolean hasErrors(final List<BuildingIssue> diagnostics) {
|
||||
for (final var diagnostic : diagnostics) {
|
||||
if (diagnostic.isError()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void printIssues(
|
||||
final ReadOnlyCollection<BuildingIssue> issues,
|
||||
final LogAggregator logs) {
|
||||
|
||||
@ -48,6 +48,11 @@ public class FrontendPhasePipelineStage implements PipelineStage {
|
||||
final var span = diagnostic.getSpan();
|
||||
issues.report(builder -> builder
|
||||
.error(diagnostic.getSeverity().isError())
|
||||
.phase(diagnostic.getPhase().id())
|
||||
.code(diagnostic.getCode())
|
||||
.fileId(span.getFileId().isNone() ? null : span.getFileId().getId())
|
||||
.start(safeToInt(span.getStart()))
|
||||
.end(safeToInt(span.getEnd()))
|
||||
.message("[BUILD] %s :: %s @ %s:[%d,%d)".formatted(
|
||||
diagnostic.getCode(),
|
||||
diagnostic.getMessage(),
|
||||
@ -56,4 +61,8 @@ public class FrontendPhasePipelineStage implements PipelineStage {
|
||||
span.getEnd())));
|
||||
}
|
||||
}
|
||||
|
||||
private int safeToInt(final long value) {
|
||||
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,12 +3,15 @@ package p.studio.compiler.integration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.messages.BuilderPipelineConfig;
|
||||
import p.studio.compiler.models.BuilderPipelineContext;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.compiler.workspaces.BuilderPipelineService;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -32,6 +35,36 @@ class MainProjectPipelineIntegrationTest {
|
||||
assertFalse(Files.exists(outputPath), "analyze must not write output: " + outputPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void analyzeShouldReturnStructuredDiagnosticsForOverlayDocumentWithoutThrowing() throws IOException {
|
||||
final var projectRoot = projectRoot();
|
||||
final var documentPath = projectRoot.resolve("src").resolve("main.pbs").toAbsolutePath().normalize();
|
||||
final var logs = bufferedLogs();
|
||||
final var invalidOverlay = """
|
||||
fn frame() -> void {
|
||||
let broken: int = ;
|
||||
}
|
||||
""";
|
||||
final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(
|
||||
false,
|
||||
projectRoot.toString(),
|
||||
"core-v1",
|
||||
SourceProviderFactory.overlayUtf8(Map.of(documentPath, invalidOverlay))));
|
||||
|
||||
final var snapshot = assertDoesNotThrow(
|
||||
() -> BuilderPipelineService.INSTANCE.analyze(context, logs),
|
||||
() -> "analyze unexpectedly failed for overlay " + documentPath);
|
||||
|
||||
final var documentDiagnostics = snapshot.diagnostics().stream()
|
||||
.filter(issue -> issue.getFileId() != null)
|
||||
.filter(issue -> issue.getFileId() >= 0 && issue.getFileId() < snapshot.fileTable().size())
|
||||
.filter(issue -> snapshot.fileTable().get(new FileId(issue.getFileId())).getCanonPath().toAbsolutePath().normalize().equals(documentPath))
|
||||
.toList();
|
||||
|
||||
assertFalse(documentDiagnostics.isEmpty(), "overlay analysis must surface diagnostics for " + documentPath);
|
||||
assertTrue(documentDiagnostics.stream().anyMatch(issue -> issue.isError() && issue.getCode() != null && issue.getStart() != null && issue.getEnd() != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException {
|
||||
final var projectRoot = projectRoot();
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package p.studio.compiler.utilities;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@FunctionalInterface
|
||||
@ -24,5 +27,38 @@ public interface SourceProviderFactory {
|
||||
};
|
||||
}
|
||||
|
||||
// more providers can be added here, e.g., in-memory, etc.
|
||||
static SourceProviderFactory overlayUtf8(final Map<Path, String> overrides) {
|
||||
final Map<Path, byte[]> bytesByPath = new HashMap<>();
|
||||
for (final var entry : Objects.requireNonNull(overrides, "overrides").entrySet()) {
|
||||
bytesByPath.put(
|
||||
normalize(entry.getKey()),
|
||||
Objects.requireNonNull(entry.getValue(), "override text").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return overlay(bytesByPath, filesystem());
|
||||
}
|
||||
|
||||
static SourceProviderFactory overlay(
|
||||
final Map<Path, byte[]> overrides,
|
||||
final SourceProviderFactory fallback) {
|
||||
final Map<Path, byte[]> normalizedOverrides = new HashMap<>();
|
||||
for (final var entry : Objects.requireNonNull(overrides, "overrides").entrySet()) {
|
||||
normalizedOverrides.put(
|
||||
normalize(entry.getKey()),
|
||||
Objects.requireNonNull(entry.getValue(), "override bytes").clone());
|
||||
}
|
||||
final SourceProviderFactory safeFallback = Objects.requireNonNull(fallback, "fallback");
|
||||
return path -> {
|
||||
final Path normalizedPath = normalize(path);
|
||||
final byte[] override = normalizedOverrides.get(normalizedPath);
|
||||
if (override != null) {
|
||||
final byte[] immutableBytes = override.clone();
|
||||
return () -> immutableBytes;
|
||||
}
|
||||
return safeFallback.create(normalizedPath);
|
||||
};
|
||||
}
|
||||
|
||||
private static Path normalize(final Path path) {
|
||||
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":prometeu-lsp:prometeu-lsp-api"))
|
||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||
implementation(libs.lsp4j)
|
||||
}
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
package p.studio.lsp.services.compiler;
|
||||
|
||||
import p.studio.compiler.messages.BuilderPipelineConfig;
|
||||
import p.studio.compiler.messages.BuildingIssue;
|
||||
import p.studio.compiler.models.AnalysisSnapshot;
|
||||
import p.studio.compiler.models.BuilderPipelineContext;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.utilities.SourceProviderFactory;
|
||||
import p.studio.compiler.workspaces.BuilderPipelineService;
|
||||
import p.studio.lsp.messages.*;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record CompilerLanguageServiceBridge(
|
||||
BuilderPipelineService builderPipelineService) implements LanguageServiceBridge {
|
||||
|
||||
public CompilerLanguageServiceBridge() {
|
||||
this(BuilderPipelineService.INSTANCE);
|
||||
}
|
||||
|
||||
public CompilerLanguageServiceBridge(final BuilderPipelineService builderPipelineService) {
|
||||
this.builderPipelineService = Objects.requireNonNull(builderPipelineService, "builderPipelineService");
|
||||
}
|
||||
public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {
|
||||
|
||||
@Override
|
||||
public BaselineServerDescription describeServer(final LspProjectContext context) {
|
||||
@ -33,16 +36,10 @@ public record CompilerLanguageServiceBridge(
|
||||
final String documentUri,
|
||||
final String text) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
// Wave 1 keeps responses intentionally synthetic while hardening the bridge boundary.
|
||||
final String message = "Compiler-backed baseline via " + builderPipelineService.getClass().getSimpleName();
|
||||
return new BaselineDocumentAnalysis(List.of(new BaselineDocumentIssue(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Math.max(1, firstLineLength(text)),
|
||||
BaselineIssueSeverity.INFORMATION,
|
||||
"Prometeu Studio",
|
||||
message)));
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final String effectiveText = text == null ? "" : text;
|
||||
final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
|
||||
return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,11 +66,115 @@ public record CompilerLanguageServiceBridge(
|
||||
return "Prometeu Studio saw save: " + documentUri;
|
||||
}
|
||||
|
||||
private int firstLineLength(final String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return 1;
|
||||
private AnalysisSnapshot analyzeProject(
|
||||
final LspProjectContext context,
|
||||
final Map<Path, String> overlays) {
|
||||
final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig(
|
||||
new BuilderPipelineConfig(
|
||||
false,
|
||||
context.projectRoot().toString(),
|
||||
"core-v1",
|
||||
SourceProviderFactory.overlayUtf8(overlays)));
|
||||
return BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
|
||||
}
|
||||
|
||||
private List<BaselineDocumentIssue> mapDiagnostics(
|
||||
final AnalysisSnapshot snapshot,
|
||||
final Path documentPath,
|
||||
final String text) {
|
||||
final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
|
||||
final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
|
||||
for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
|
||||
if (diagnostic.getFileId() == null || diagnostic.getFileId() < 0) {
|
||||
continue;
|
||||
}
|
||||
final int fileIndex = diagnostic.getFileId();
|
||||
if (fileIndex >= snapshot.fileTable().size()) {
|
||||
continue;
|
||||
}
|
||||
final var sourceHandle = snapshot.fileTable().get(new FileId(fileIndex));
|
||||
if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
continue;
|
||||
}
|
||||
issues.add(toBaselineIssue(diagnostic, positionMapper));
|
||||
}
|
||||
return List.copyOf(issues);
|
||||
}
|
||||
|
||||
private BaselineDocumentIssue toBaselineIssue(
|
||||
final BuildingIssue diagnostic,
|
||||
final DocumentPositionMapper positionMapper) {
|
||||
final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
|
||||
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
|
||||
final DocumentPosition start = positionMapper.positionOf(startOffset);
|
||||
final DocumentPosition end = positionMapper.positionOf(endOffset);
|
||||
return new BaselineDocumentIssue(
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
|
||||
diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
|
||||
diagnostic.getMessage());
|
||||
}
|
||||
|
||||
private Path normalizeDocumentPath(final String documentUri) {
|
||||
return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private record DocumentPosition(
|
||||
int line,
|
||||
int character) {
|
||||
}
|
||||
|
||||
private static final class DocumentPositionMapper {
|
||||
private final String text;
|
||||
|
||||
private DocumentPositionMapper(final String text) {
|
||||
this.text = Objects.requireNonNull(text, "text");
|
||||
}
|
||||
|
||||
private DocumentPosition positionOf(final Integer byteOffset) {
|
||||
if (byteOffset == null || byteOffset <= 0 || text.isEmpty()) {
|
||||
return new DocumentPosition(0, 0);
|
||||
}
|
||||
final int target = Math.min(byteOffset, text.getBytes(StandardCharsets.UTF_8).length);
|
||||
int utf8Offset = 0;
|
||||
int line = 0;
|
||||
int character = 0;
|
||||
for (int index = 0; index < text.length(); ) {
|
||||
if (utf8Offset >= target) {
|
||||
return new DocumentPosition(line, character);
|
||||
}
|
||||
final int codePoint = text.codePointAt(index);
|
||||
final int utf16Width = Character.charCount(codePoint);
|
||||
final int utf8Width = utf8Length(codePoint);
|
||||
if (utf8Offset + utf8Width > target) {
|
||||
return new DocumentPosition(line, character);
|
||||
}
|
||||
utf8Offset += utf8Width;
|
||||
if (codePoint == '\n') {
|
||||
line++;
|
||||
character = 0;
|
||||
} else {
|
||||
character += utf16Width;
|
||||
}
|
||||
index += utf16Width;
|
||||
}
|
||||
return new DocumentPosition(line, character);
|
||||
}
|
||||
|
||||
private int utf8Length(final int codePoint) {
|
||||
if (codePoint <= 0x7F) {
|
||||
return 1;
|
||||
}
|
||||
if (codePoint <= 0x7FF) {
|
||||
return 2;
|
||||
}
|
||||
if (codePoint <= 0xFFFF) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
final int newline = text.indexOf('\n');
|
||||
return newline < 0 ? text.length() : newline;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.LanguageClientAware;
|
||||
import org.eclipse.lsp4j.services.LanguageServer;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import org.eclipse.lsp4j.services.WorkspaceService;
|
||||
import org.eclipse.lsp4j.InitializeParams;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.InitializedParams;
|
||||
import org.eclipse.lsp4j.services.*;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.Lsp4jProtocolMessageMapper;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
|
||||
@ -64,13 +62,13 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
|
||||
} catch (RuntimeException runtimeException) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient != null) {
|
||||
currentClient.logMessage(protocolMessageMapper.mapInfoMessage(
|
||||
"Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage()));
|
||||
currentClient.logMessage(protocolMessageMapper
|
||||
.mapErrorMessage("Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage()));
|
||||
}
|
||||
return CompletableFuture.failedFuture(runtimeException);
|
||||
}
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapInitializeResult(
|
||||
languageServiceBridge.describeServer(project)));
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper
|
||||
.mapInitializeResult(languageServiceBridge.describeServer(project)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -79,8 +77,8 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
|
||||
if (currentClient == null) {
|
||||
return;
|
||||
}
|
||||
currentClient.logMessage(protocolMessageMapper.mapInfoMessage(
|
||||
"Prometeu Studio LSP connected for project " + project.projectKey()));
|
||||
currentClient.logMessage(protocolMessageMapper
|
||||
.mapInfoMessage("Prometeu Studio LSP connected for project " + project.projectKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -94,8 +92,8 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
|
||||
if (!shutdownRequested) {
|
||||
final LanguageClient currentClient = client;
|
||||
if (currentClient != null) {
|
||||
currentClient.logMessage(new MessageParams(MessageType.Warning,
|
||||
"Prometeu Studio LSP received exit before shutdown"));
|
||||
currentClient.logMessage(protocolMessageMapper
|
||||
.mapWarningMessage("Prometeu Studio LSP received exit before shutdown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,6 +62,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return new MessageParams(MessageType.Error, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageParams mapWarningMessage(String message) {
|
||||
return new MessageParams(MessageType.Warning, message);
|
||||
}
|
||||
|
||||
private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) {
|
||||
final Diagnostic diagnostic = new Diagnostic();
|
||||
diagnostic.setRange(new Range(
|
||||
|
||||
@ -19,4 +19,5 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
MessageParams mapInfoMessage(String message);
|
||||
MessageParams mapErrorMessage(String message);
|
||||
MessageParams mapWarningMessage(String message);
|
||||
}
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
package p.studio.lsp.services.compiler;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CompilerLanguageServiceBridgeTest {
|
||||
|
||||
@Test
|
||||
void analyzeDocumentReturnsCompilerDiagnosticsForOverlayContent() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
.resolve("test-projects")
|
||||
.resolve("main")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
|
||||
final String invalidOverlay = """
|
||||
fn frame() -> void {
|
||||
let broken: int = ;
|
||||
}
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
final var analysis = bridge.analyzeDocument(
|
||||
new LspProjectContext("main", "pbs", projectRoot),
|
||||
documentPath.toUri().toString(),
|
||||
invalidOverlay);
|
||||
|
||||
assertFalse(analysis.issues().isEmpty(), "analyzeDocument must surface compiler diagnostics");
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR));
|
||||
assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1));
|
||||
assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline")));
|
||||
}
|
||||
|
||||
private Path findRepoRoot(final Path start) {
|
||||
var current = start;
|
||||
while (current != null) {
|
||||
if (Files.exists(current.resolve("settings.gradle.kts"))
|
||||
&& Files.exists(current.resolve("test-projects").resolve("main").resolve("prometeu.json"))) {
|
||||
return current;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
throw new IllegalStateException("unable to locate repository root from " + start);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.Hover;
|
||||
import org.eclipse.lsp4j.InitializeParams;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.InitializedParams;
|
||||
import org.eclipse.lsp4j.MessageParams;
|
||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PrometeuLanguageServerTest {
|
||||
|
||||
@Test
|
||||
void initializeDelegatesThroughBridgeAndMapper() {
|
||||
final RecordingBridge bridge = new RecordingBridge();
|
||||
final RecordingMapper mapper = new RecordingMapper();
|
||||
final InitializeResult expected = new InitializeResult();
|
||||
mapper.initializeResult = expected;
|
||||
|
||||
final PrometeuLanguageServer server = new PrometeuLanguageServer(
|
||||
new LspProjectContext("demo", "pbs", Path.of(".")),
|
||||
bridge,
|
||||
mapper,
|
||||
new NoopTextDocumentService(),
|
||||
new PrometeuWorkspaceService());
|
||||
|
||||
final InitializeParams params = new InitializeParams();
|
||||
params.setRootUri(Path.of(".").toAbsolutePath().normalize().toUri().toString());
|
||||
|
||||
final InitializeResult result = server.initialize(params).join();
|
||||
|
||||
assertSame(expected, result);
|
||||
assertEquals(1, bridge.describeServerCalls);
|
||||
assertSame(bridge.serverDescription, mapper.lastServerDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializeRejectsClientBoundToDifferentProjectRoot() {
|
||||
final RecordingBridge bridge = new RecordingBridge();
|
||||
final RecordingMapper mapper = new RecordingMapper();
|
||||
final PrometeuLanguageServer server = new PrometeuLanguageServer(
|
||||
new LspProjectContext("demo", "pbs", Path.of("/tmp/studio-project")),
|
||||
bridge,
|
||||
mapper,
|
||||
new NoopTextDocumentService(),
|
||||
new PrometeuWorkspaceService());
|
||||
|
||||
final InitializeParams params = new InitializeParams();
|
||||
params.setRootUri(Path.of("/tmp/another-project").toUri().toString());
|
||||
|
||||
final CompletionException failure = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
CompletionException.class,
|
||||
() -> server.initialize(params).join());
|
||||
|
||||
final ResponseErrorException responseError = assertInstanceOf(ResponseErrorException.class, failure.getCause());
|
||||
assertTrue(responseError.getMessage().contains("/tmp/studio-project"));
|
||||
assertEquals(0, bridge.describeServerCalls);
|
||||
}
|
||||
|
||||
private static final class RecordingBridge implements LanguageServiceBridge {
|
||||
private int describeServerCalls;
|
||||
private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true);
|
||||
|
||||
@Override
|
||||
public BaselineServerDescription describeServer(final LspProjectContext project) {
|
||||
describeServerCalls++;
|
||||
return serverDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineDocumentAnalysis analyzeDocument(final LspProjectContext project, final String documentUri, final String text) {
|
||||
return new BaselineDocumentAnalysis(List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineHover hover(final LspProjectContext project, final String documentUri, final int line, final int character) {
|
||||
return new BaselineHover("hover");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext project, final String documentUri) {
|
||||
return "saved";
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingMapper implements ProtocolMessageMapper {
|
||||
private InitializeResult initializeResult;
|
||||
private BaselineServerDescription lastServerDescription;
|
||||
|
||||
@Override
|
||||
public InitializeResult mapInitializeResult(final BaselineServerDescription description) {
|
||||
lastServerDescription = description;
|
||||
return initializeResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublishDiagnosticsParams mapDiagnostics(final String uri, final BaselineDocumentAnalysis analysis) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublishDiagnosticsParams emptyDiagnostics(final String uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hover mapHover(final BaselineHover hover) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageParams mapInfoMessage(final String message) {
|
||||
final MessageParams params = new MessageParams();
|
||||
params.setMessage(message);
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageParams mapErrorMessage(final String message) {
|
||||
final MessageParams params = new MessageParams();
|
||||
params.setMessage(message);
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageParams mapWarningMessage(final String message) {
|
||||
final MessageParams params = new MessageParams();
|
||||
params.setMessage(message);
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoopTextDocumentService implements TextDocumentService {
|
||||
@Override
|
||||
public void didOpen(final org.eclipse.lsp4j.DidOpenTextDocumentParams params) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChange(final org.eclipse.lsp4j.DidChangeTextDocumentParams params) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didClose(final org.eclipse.lsp4j.DidCloseTextDocumentParams params) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didSave(final org.eclipse.lsp4j.DidSaveTextDocumentParams params) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user