Files
voidraft/frontend/src/common/prettier/plugins/scala/visitor/utils.ts

296 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ScalaCstNode, IToken } from "../scala-parser";
/**
* ビジターパターンで使用する共有ユーティリティとフォーマットヘルパー
*/
export interface PrettierOptions {
printWidth?: number;
tabWidth?: number;
useTabs?: boolean;
semi?: boolean;
singleQuote?: boolean;
trailingComma?: "all" | "multiline" | "none";
scalaLineWidth?: number; // Deprecated, for backward compatibility
}
// CST要素ードまたはトークンのユニオン型
export type CSTNode = ScalaCstNode | IToken;
export type PrintContext = {
path: unknown;
options: PrettierOptions;
print: (node: CSTNode) => string;
indentLevel: number;
};
/**
* nullチェック付きでードの子要素に安全にアクセス
* @param node - 対象ノード
* @returns 子要素のマップ
*/
export function getChildren(node: CSTNode): Record<string, CSTNode[]> {
if ("children" in node && node.children) {
return node.children as Record<string, CSTNode[]>;
}
return {};
}
/**
* キーで指定した子ノードを安全に取得
* @param node - 対象ノード
* @param key - 子ノードのキー
* @returns 子ノードの配列
*/
export function getChildNodes(node: CSTNode, key: string): CSTNode[] {
return getChildren(node)[key] || [];
}
/**
* キーで指定した最初の子ノードを安全に取得
* @param node - 対象ノード
* @param key - 子ノードのキー
* @returns 最初の子ードまたはundefined
*/
export function getFirstChild(node: CSTNode, key: string): CSTNode | undefined {
const children = getChildNodes(node, key);
return children.length > 0 ? children[0] : undefined;
}
/**
* ードのimageプロパティを安全に取得
* @param node - 対象ノード
* @returns imageプロパティまたは空文字列
*/
export function getNodeImage(node: CSTNode): string {
if ("image" in node && node.image) {
return node.image;
}
return "";
}
/**
* nullまたはundefinedの可能性があるードのimageを安全に取得
* @param node - 対象ードnull/undefined可
* @returns imageプロパティまたは空文字列
*/
export function getNodeImageSafe(node: CSTNode | undefined | null): string {
if (node && "image" in node && node.image) {
return node.image;
}
return "";
}
/**
* 有効なprintWidthを取得scalafmt互換性をサポート
* @param ctx - 印刷コンテキスト
* @returns 有効な行幅
*/
export function getPrintWidth(ctx: PrintContext): number {
// PrettierのprintWidthを使用scalafmtのmaxColumn互換
if (ctx.options.printWidth) {
return ctx.options.printWidth;
}
// 後方互換性のため非推奨のscalaLineWidthにフォールバック
if (ctx.options.scalaLineWidth) {
// 開発環境で非推奨警告を表示
if (process.env.NODE_ENV !== "production") {
console.warn(
"scalaLineWidth is deprecated. Use printWidth instead for scalafmt compatibility.",
);
}
return ctx.options.scalaLineWidth;
}
// デフォルト値
return 80;
}
/**
* 有効なtabWidthを取得scalafmt互換性をサポート
* @param ctx - 印刷コンテキスト
* @returns 有効なタブ幅
*/
export function getTabWidth(ctx: PrintContext): number {
// PrettierのtabWidthを使用scalafmtのindent.main互換
if (ctx.options.tabWidth) {
return ctx.options.tabWidth;
}
// デフォルト値
return 2;
}
/**
* セミコロンのフォーマットを処理Prettierのsemiオプションをサポート
* @param statement - ステートメント文字列
* @param ctx - 印刷コンテキスト
* @returns フォーマット済みのステートメント
*/
export function formatStatement(statement: string, ctx: PrintContext): string {
// Prettierのsemiオプションを使用
// プラグインはScala用にデフォルトsemi=falseを設定するが、明示的なユーザー選択を尊重
const useSemi = ctx.options.semi === true;
// 既存の末尾セミコロンを削除
const cleanStatement = statement.replace(/;\s*$/, "");
// リクエストされた場合セミコロンを追加
if (useSemi) {
return cleanStatement + ";";
}
return cleanStatement;
}
/**
* 文字列クォートのフォーマットを処理PrettierのsingleQuoteオプションをサポート
* @param content - 文字列リテラルの内容
* @param ctx - 印刷コンテキスト
* @returns フォーマット済みの文字列
*/
export function formatStringLiteral(
content: string,
ctx: PrintContext,
): string {
// PrettierのsingleQuoteオプションを使用
const useSingleQuote = ctx.options.singleQuote === true;
// 文字列補間をスキップs"、f"、raw"などで始まる)
if (content.match(/^[a-zA-Z]"/)) {
return content; // 補間文字列は変更しない
}
// 内容を抽出
let innerContent = content;
if (content.startsWith('"') && content.endsWith('"')) {
innerContent = content.slice(1, -1);
} else if (content.startsWith("'") && content.endsWith("'")) {
innerContent = content.slice(1, -1);
} else {
return content; // Not a string literal
}
// Choose target quote based on option
const targetQuote = useSingleQuote ? "'" : '"';
// Handle escaping if necessary
if (targetQuote === "'") {
// Converting to single quotes: escape single quotes, unescape double quotes
innerContent = innerContent.replace(/\\"/g, '"').replace(/'/g, "\\'");
} else {
// Converting to double quotes: escape double quotes, unescape single quotes
innerContent = innerContent.replace(/\\'/g, "'").replace(/"/g, '\\"');
}
return targetQuote + innerContent + targetQuote;
}
/**
* Helper method to handle indentation using configured tab width
*/
export function createIndent(level: number, ctx: PrintContext): string {
const tabWidth = getTabWidth(ctx);
const useTabs = ctx.options.useTabs === true;
if (useTabs) {
return "\t".repeat(level);
} else {
return " ".repeat(level * tabWidth);
}
}
/**
* Helper method to handle trailing comma formatting
*/
export function formatTrailingComma(
elements: string[],
ctx: PrintContext,
isMultiline: boolean = false,
): string {
if (elements.length === 0) return "";
const trailingComma = ctx.options.trailingComma;
if (
trailingComma === "all" ||
(trailingComma === "multiline" && isMultiline)
) {
return elements.join(", ") + ",";
}
return elements.join(", ");
}
/**
* Attach original comments to the formatted result
*/
export function attachOriginalComments(
result: string,
originalComments: CSTNode[],
): string {
if (!originalComments || originalComments.length === 0) {
return result;
}
const lines = result.split("\n");
const commentMap = new Map<number, string[]>();
// Group comments by line number
originalComments.forEach((comment) => {
const line = ("startLine" in comment && comment.startLine) || 1;
if (!commentMap.has(line)) {
commentMap.set(line, []);
}
let commentText = "";
if ("image" in comment && comment.image) {
commentText = comment.image;
} else if ("value" in comment && comment.value) {
commentText = String(comment.value);
}
if (commentText) {
commentMap.get(line)!.push(commentText);
}
});
// Insert comments into lines
const resultLines: string[] = [];
lines.forEach((line, index) => {
const lineNumber = index + 1;
if (commentMap.has(lineNumber)) {
const comments = commentMap.get(lineNumber)!;
comments.forEach((comment) => {
resultLines.push(comment);
});
}
resultLines.push(line);
});
return resultLines.join("\n");
}
/**
* Format method or class parameters with proper line breaks
*/
export function formatParameterList(
parameters: CSTNode[],
ctx: PrintContext,
visitFn: (param: CSTNode, ctx: PrintContext) => string,
): string {
if (parameters.length === 0) return "";
const paramStrings = parameters.map((param) => visitFn(param, ctx));
const printWidth = getPrintWidth(ctx);
const joined = paramStrings.join(", ");
// If the line is too long, break into multiple lines
if (joined.length > printWidth * 0.7) {
const indent = createIndent(1, ctx);
return "\n" + indent + paramStrings.join(",\n" + indent) + "\n";
}
return joined;
}