/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {randomId, UUID} from "./uuid";
import {asRef} from "./reference";
export const MarkersKind = {
Markers: "org.openrewrite.marker.Markers",
NamedStyles: "org.openrewrite.style.NamedStyles",
SearchResult: "org.openrewrite.marker.SearchResult",
ParseExceptionResult: "org.openrewrite.ParseExceptionResult",
// Markup markers for errors, warnings, info, and debug messages
MarkupError: "org.openrewrite.marker.Markup$Error",
MarkupWarn: "org.openrewrite.marker.Markup$Warn",
MarkupInfo: "org.openrewrite.marker.Markup$Info",
MarkupDebug: "org.openrewrite.marker.Markup$Debug",
RecipesThatMadeChanges: "org.openrewrite.marker.RecipesThatMadeChanges",
RecipeThatMadeChanges: "org.openrewrite.marker.RecipeThatMadeChanges",
/**
* A generic marker that is sent/received as a bare map because the type hasn't been
* defined in both Java and JavaScript.
*/
RpcMarker: "org.openrewrite.rpc.RpcMarker",
} as const
export interface Marker {
readonly kind: string
readonly id: UUID
}
export function marker(id: UUID, data?: {}): Marker {
return {
kind: MarkersKind.RpcMarker,
id,
...data
}
}
export function markers(...markers: Marker[]): Markers {
return {
kind: MarkersKind.Markers,
id: randomId(),
markers
}
}
export interface Markers {
readonly kind: typeof MarkersKind.Markers
readonly id: UUID
readonly markers: Marker[]
}
export function findMarker(
o: { markers: Markers },
kind: T["kind"]
): T | undefined {
return o.markers.markers.find(
(m): m is T => m.kind === kind
);
}
/**
* Replaces a marker in a Markers collection with a new marker.
* If the old marker is not found, the new marker is added.
*
* @param markers The markers collection to update
* @param oldMarker The marker to replace (matched by id)
* @param newMarker The new marker to insert
* @returns A new Markers object with the replacement applied
*/
export function replaceMarker(markers: Markers, oldMarker: Marker, newMarker: Marker): Markers {
const newMarkers = markers.markers.map(m =>
m.id === oldMarker.id ? newMarker : m
);
// If old marker wasn't found, add the new one
if (!markers.markers.some(m => m.id === oldMarker.id)) {
newMarkers.push(newMarker);
}
return {
...markers,
markers: newMarkers
};
}
/**
* Replaces the first marker with the same kind as the new marker, or adds it if not found.
* This is useful when there's typically only one marker of each kind.
*
* @param markers The markers collection to update
* @param newMarker The new marker to insert (its kind is used to find the marker to replace)
* @returns A new Markers object with the replacement applied
*/
export function replaceMarkerByKind(markers: Markers, newMarker: Marker): Markers {
let found = false;
const newMarkers = markers.markers.map(m => {
if (!found && m.kind === newMarker.kind) {
found = true;
return newMarker;
}
return m;
});
// If marker with kind wasn't found, add the new one
if (!found) {
newMarkers.push(newMarker);
}
return {
...markers,
markers: newMarkers
};
}
export const emptyMarkers: Markers = asRef({
kind: MarkersKind.Markers,
id: randomId(),
markers: []
});
/**
* One frame of a {@link RecipesThatMadeChanges} stack: how the recipe is named, how it was
* configured, and what it is worth.
*/
export interface RecipeThatMadeChanges {
readonly kind: typeof MarkersKind.RecipeThatMadeChanges
readonly name: string
readonly displayName?: string
readonly instanceName?: string
/**
* Configured option values. JavaScript never interprets them.
*/
readonly options?: { [name: string]: any }
readonly estimatedEffortPerOccurrenceMillis?: number
}
/**
* Records which recipe stacks changed a source file. JavaScript holds the stacks without
* interpreting them, so a marker served to this peer returns to the host intact.
*/
export interface RecipesThatMadeChanges extends Marker {
readonly kind: typeof MarkersKind.RecipesThatMadeChanges
readonly recipes: RecipeThatMadeChanges[][]
}
export interface SearchResult extends Marker {
readonly kind: typeof MarkersKind.SearchResult,
readonly description?: string
}
export function foundSearchResult(t: T, description?: string): T {
const existing = findMarker(t, MarkersKind.SearchResult);
if (!existing || (existing as SearchResult).description !== description) {
return {
...t,
markers: {
...t.markers,
markers: [...t.markers.markers, {
kind: MarkersKind.SearchResult,
id: randomId(),
description: description
} as SearchResult]
}
} as T;
}
return t;
}
export interface ParseExceptionResult extends Marker {
readonly kind: typeof MarkersKind.ParseExceptionResult
readonly parserType: string
readonly exceptionType: string
readonly message: string
readonly treeType?: string;
}
/**
* Base interface for Markup markers that attach messages to AST nodes.
* Used for errors, warnings, info, and debug messages.
*/
export interface Markup extends Marker {
readonly message: string;
readonly detail?: string;
}
export interface MarkupError extends Markup {
readonly kind: typeof MarkersKind.MarkupError;
}
export interface MarkupWarn extends Markup {
readonly kind: typeof MarkersKind.MarkupWarn;
}
export interface MarkupInfo extends Markup {
readonly kind: typeof MarkersKind.MarkupInfo;
}
export interface MarkupDebug extends Markup {
readonly kind: typeof MarkersKind.MarkupDebug;
}
/**
* Attaches an error marker to a tree node.
*/
export function markupError(t: T, message: string, detail?: string): T {
return addMarkup(t, {
kind: MarkersKind.MarkupError,
id: randomId(),
message,
detail
});
}
/**
* Attaches a warning marker to a tree node.
*/
export function markupWarn(t: T, message: string, detail?: string): T {
return addMarkup(t, {
kind: MarkersKind.MarkupWarn,
id: randomId(),
message,
detail
});
}
/**
* Attaches an info marker to a tree node.
*/
export function markupInfo(t: T, message: string, detail?: string): T {
return addMarkup(t, {
kind: MarkersKind.MarkupInfo,
id: randomId(),
message,
detail
});
}
/**
* Attaches a debug marker to a tree node.
*/
export function markupDebug(t: T, message: string, detail?: string): T {
return addMarkup(t, {
kind: MarkersKind.MarkupDebug,
id: randomId(),
message,
detail
});
}
/**
* Helper to add a markup marker to a tree node.
*/
function addMarkup(t: T, markup: Markup): T {
return {
...t,
markers: {
...t.markers,
markers: [...t.markers.markers, markup]
}
} as T;
}