basic for analyse

This commit is contained in:
bQUARKz 2026-05-06 03:40:55 +01:00
parent 41f2c804a0
commit 85a224fc7b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 466 additions and 46 deletions

View File

@ -57,7 +57,7 @@ public class BuilderPipelineService {
public AnalysisSnapshot analyze( public AnalysisSnapshot analyze(
final BuilderPipelineContext ctx, final BuilderPipelineContext ctx,
final LogAggregator logs) { 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( return new AnalysisSnapshot(
ctx.resolvedWorkspace.frontendSpec(), ctx.resolvedWorkspace.frontendSpec(),
diagnostics, diagnostics,
@ -70,7 +70,10 @@ public class BuilderPipelineService {
final BuilderPipelineContext ctx, final BuilderPipelineContext ctx,
final LogAggregator logs) { final LogAggregator logs) {
final var analysisSnapshot = this.analyze(ctx, 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( return new CompileResult(
analysisSnapshot, analysisSnapshot,
diagnostics, diagnostics,
@ -84,7 +87,7 @@ public class BuilderPipelineService {
final BuilderPipelineContext ctx, final BuilderPipelineContext ctx,
final LogAggregator logs) { final LogAggregator logs) {
final var compileResult = this.compile(ctx, 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( return new BuildResult(
compileResult, compileResult,
diagnostics, diagnostics,
@ -95,19 +98,32 @@ public class BuilderPipelineService {
final BuilderPipelineContext ctx, final BuilderPipelineContext ctx,
final LogAggregator logs, final LogAggregator logs,
final List<PipelineStage> stages, final List<PipelineStage> stages,
final List<BuildingIssue> diagnostics) { final List<BuildingIssue> diagnostics,
final boolean failFast) {
for (final var builderPipelineStage : stages) { for (final var builderPipelineStage : stages) {
final var issues = builderPipelineStage.run(ctx, logs); final var issues = builderPipelineStage.run(ctx, logs);
diagnostics.addAll(issues.asCollection()); diagnostics.addAll(issues.asCollection());
printIssues(issues, logs); printIssues(issues, logs);
if (issues.hasErrors()) { if (issues.hasErrors()) {
if (failFast) {
throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName()); throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName());
} }
break;
}
} }
logs.using(log).info("builder pipeline completed successfully"); logs.using(log).info("builder pipeline completed successfully");
return List.copyOf(diagnostics); 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( private void printIssues(
final ReadOnlyCollection<BuildingIssue> issues, final ReadOnlyCollection<BuildingIssue> issues,
final LogAggregator logs) { final LogAggregator logs) {

View File

@ -48,6 +48,11 @@ public class FrontendPhasePipelineStage implements PipelineStage {
final var span = diagnostic.getSpan(); final var span = diagnostic.getSpan();
issues.report(builder -> builder issues.report(builder -> builder
.error(diagnostic.getSeverity().isError()) .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( .message("[BUILD] %s :: %s @ %s:[%d,%d)".formatted(
diagnostic.getCode(), diagnostic.getCode(),
diagnostic.getMessage(), diagnostic.getMessage(),
@ -56,4 +61,8 @@ public class FrontendPhasePipelineStage implements PipelineStage {
span.getEnd()))); span.getEnd())));
} }
} }
private int safeToInt(final long value) {
return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
}
} }

View File

@ -3,12 +3,15 @@ package p.studio.compiler.integration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.BuilderPipelineConfig; import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.models.BuilderPipelineContext; 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.compiler.workspaces.BuilderPipelineService;
import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.logs.LogAggregator;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -32,6 +35,36 @@ class MainProjectPipelineIntegrationTest {
assertFalse(Files.exists(outputPath), "analyze must not write output: " + outputPath); 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 @Test
void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException { void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException {
final var projectRoot = projectRoot(); final var projectRoot = projectRoot();

View File

@ -1,8 +1,11 @@
package p.studio.compiler.utilities; package p.studio.compiler.utilities;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
@FunctionalInterface @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();
}
} }

View File

@ -4,6 +4,7 @@ plugins {
dependencies { dependencies {
implementation(project(":prometeu-lsp:prometeu-lsp-api")) implementation(project(":prometeu-lsp:prometeu-lsp-api"))
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
implementation(project(":prometeu-compiler:prometeu-build-pipeline")) implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
implementation(libs.lsp4j) implementation(libs.lsp4j)
} }

View File

@ -1,22 +1,25 @@
package p.studio.lsp.services.compiler; 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.compiler.workspaces.BuilderPipelineService;
import p.studio.lsp.messages.*; import p.studio.lsp.messages.*;
import p.studio.lsp.services.LanguageServiceBridge; 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.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
public record CompilerLanguageServiceBridge( public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {
BuilderPipelineService builderPipelineService) implements LanguageServiceBridge {
public CompilerLanguageServiceBridge() {
this(BuilderPipelineService.INSTANCE);
}
public CompilerLanguageServiceBridge(final BuilderPipelineService builderPipelineService) {
this.builderPipelineService = Objects.requireNonNull(builderPipelineService, "builderPipelineService");
}
@Override @Override
public BaselineServerDescription describeServer(final LspProjectContext context) { public BaselineServerDescription describeServer(final LspProjectContext context) {
@ -33,16 +36,10 @@ public record CompilerLanguageServiceBridge(
final String documentUri, final String documentUri,
final String text) { final String text) {
Objects.requireNonNull(context, "context"); Objects.requireNonNull(context, "context");
// Wave 1 keeps responses intentionally synthetic while hardening the bridge boundary. final Path documentPath = normalizeDocumentPath(documentUri);
final String message = "Compiler-backed baseline via " + builderPipelineService.getClass().getSimpleName(); final String effectiveText = text == null ? "" : text;
return new BaselineDocumentAnalysis(List.of(new BaselineDocumentIssue( final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
0, return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
0,
0,
Math.max(1, firstLineLength(text)),
BaselineIssueSeverity.INFORMATION,
"Prometeu Studio",
message)));
} }
@Override @Override
@ -69,11 +66,115 @@ public record CompilerLanguageServiceBridge(
return "Prometeu Studio saw save: " + documentUri; return "Prometeu Studio saw save: " + documentUri;
} }
private int firstLineLength(final String text) { private AnalysisSnapshot analyzeProject(
if (text == null || text.isEmpty()) { 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; return 1;
} }
final int newline = text.indexOf('\n'); if (codePoint <= 0x7FF) {
return newline < 0 ? text.length() : newline; return 2;
}
if (codePoint <= 0xFFFF) {
return 3;
}
return 4;
}
} }
} }

View File

@ -1,14 +1,12 @@
package p.studio.lsp.services.protocol; package p.studio.lsp.services.protocol;
import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.lsp4j.services.*;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import p.studio.lsp.messages.LspProjectContext; import p.studio.lsp.messages.LspProjectContext;
import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge;
import p.studio.lsp.services.LanguageServiceBridge; 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.Lsp4jProtocolMessageMapper;
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
@ -64,13 +62,13 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
} catch (RuntimeException runtimeException) { } catch (RuntimeException runtimeException) {
final LanguageClient currentClient = client; final LanguageClient currentClient = client;
if (currentClient != null) { if (currentClient != null) {
currentClient.logMessage(protocolMessageMapper.mapInfoMessage( currentClient.logMessage(protocolMessageMapper
"Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage())); .mapErrorMessage("Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage()));
} }
return CompletableFuture.failedFuture(runtimeException); return CompletableFuture.failedFuture(runtimeException);
} }
return CompletableFuture.completedFuture(protocolMessageMapper.mapInitializeResult( return CompletableFuture.completedFuture(protocolMessageMapper
languageServiceBridge.describeServer(project))); .mapInitializeResult(languageServiceBridge.describeServer(project)));
} }
@Override @Override
@ -79,8 +77,8 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
if (currentClient == null) { if (currentClient == null) {
return; return;
} }
currentClient.logMessage(protocolMessageMapper.mapInfoMessage( currentClient.logMessage(protocolMessageMapper
"Prometeu Studio LSP connected for project " + project.projectKey())); .mapInfoMessage("Prometeu Studio LSP connected for project " + project.projectKey()));
} }
@Override @Override
@ -94,8 +92,8 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
if (!shutdownRequested) { if (!shutdownRequested) {
final LanguageClient currentClient = client; final LanguageClient currentClient = client;
if (currentClient != null) { if (currentClient != null) {
currentClient.logMessage(new MessageParams(MessageType.Warning, currentClient.logMessage(protocolMessageMapper
"Prometeu Studio LSP received exit before shutdown")); .mapWarningMessage("Prometeu Studio LSP received exit before shutdown"));
} }
} }
} }

View File

@ -62,6 +62,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return new MessageParams(MessageType.Error, message); return new MessageParams(MessageType.Error, message);
} }
@Override
public MessageParams mapWarningMessage(String message) {
return new MessageParams(MessageType.Warning, message);
}
private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) { private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) {
final Diagnostic diagnostic = new Diagnostic(); final Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range( diagnostic.setRange(new Range(

View File

@ -19,4 +19,5 @@ public interface ProtocolMessageMapper {
MessageParams mapInfoMessage(String message); MessageParams mapInfoMessage(String message);
MessageParams mapErrorMessage(String message); MessageParams mapErrorMessage(String message);
MessageParams mapWarningMessage(String message);
} }

View File

@ -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);
}
}

View File

@ -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) {
}
}
}