2 Commits

Author SHA1 Message Date
338ac358db 🚧 Modify toml,powershell prettier plugin(beta) 2025-09-17 00:12:39 +08:00
a83c7139c9 Added scala、powershell、groovy prettier plugin 2025-09-14 23:45:01 +08:00
44 changed files with 14390 additions and 316 deletions

View File

@@ -37,27 +37,29 @@
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.2",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@prettier/plugin-xml": "^3.4.2",
"@reteps/dockerfmt": "^0.3.6",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"groovy-beautify": "^0.0.17",
"hsl-matcher": "^1.2.4",
"java-parser": "^3.0.1",
"jinx-rust": "^0.1.6",
"jsox": "^1.2.123",
"lezer": "^0.13.5",
"linguist-languages": "^9.0.0",
"node-sql-parser": "^5.3.12",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"prettier": "^3.6.2",
"prettier-plugin-toml": "^2.0.6",
"remarkable": "^2.0.1",
"sass": "^1.92.1",
"sh-syntax": "^0.5.8",
@@ -569,6 +571,12 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cospaia/prettier-plugin-clojure": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@cospaia/prettier-plugin-clojure/-/prettier-plugin-clojure-0.0.2.tgz",
"integrity": "sha512-5ZgNOdiiIHbcBLvJhonCGoHFfuLlfsA+CjohiZGVuyD2XMVi35YFr7vZ6eSHeWjFAUsKRFbcOqtoXsV1Wk7zXA==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
@@ -2228,19 +2236,23 @@
"win32"
]
},
"node_modules/@taplo/core": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/@taplo/core/-/core-0.2.0.tgz",
"integrity": "sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==",
"license": "MIT"
},
"node_modules/@taplo/lib": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/@taplo/lib/-/lib-0.5.0.tgz",
"integrity": "sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==",
"node_modules/@toml-tools/lexer": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz",
"integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==",
"license": "MIT",
"dependencies": {
"@taplo/core": "^0.2.0"
"chevrotain": "^11.0.1"
}
},
"node_modules/@toml-tools/parser": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz",
"integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==",
"license": "MIT",
"dependencies": {
"@toml-tools/lexer": "^1.0.0",
"chevrotain": "^11.0.1"
}
},
"node_modules/@types/estree": {
@@ -4545,6 +4557,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/groovy-beautify": {
"version": "0.0.17",
"resolved": "https://registry.npmmirror.com/groovy-beautify/-/groovy-beautify-0.0.17.tgz",
"integrity": "sha512-n3GRn7wJMCoPpNOC9bhuHWxnTkb9CwVnQH1RJK4M/F3Edc7l2FOa7wLa8iL2eqt0sQgQLzbxSsvZ7En2fJ8ZUg==",
"license": "MIT"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -5113,16 +5131,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/lezer": {
"version": "0.13.5",
"resolved": "https://registry.npmmirror.com/lezer/-/lezer-0.13.5.tgz",
"integrity": "sha512-cAiMQZGUo2BD8mpcz7Nv1TlKzWP7YIdIRrX41CiP5bk5t4GHxskOxWUx2iAOuHlz8dO+ivbuXr0J1bfHsWD+lQ==",
"deprecated": "This package has been replaced by @lezer/lr",
"license": "MIT",
"dependencies": {
"lezer-tree": "^0.13.2"
}
},
"node_modules/lezer-elixir": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.2.tgz",
@@ -5132,13 +5140,6 @@
"@lezer/lr": "^1.3.0"
}
},
"node_modules/lezer-tree": {
"version": "0.13.2",
"resolved": "https://registry.npmmirror.com/lezer-tree/-/lezer-tree-0.13.2.tgz",
"integrity": "sha512-15ZxW8TxVNAOkHIo43Iouv4zbSkQQ5chQHBpwXcD2bBFz46RB4jYLEEww5l1V0xyIx9U2clSyyrLes+hAUFrGQ==",
"deprecated": "This package has been replaced by @lezer/common",
"license": "MIT"
},
"node_modules/linguist-languages": {
"version": "9.0.0",
"resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.0.0.tgz",
@@ -5990,24 +5991,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-toml": {
"version": "2.0.6",
"resolved": "https://registry.npmmirror.com/prettier-plugin-toml/-/prettier-plugin-toml-2.0.6.tgz",
"integrity": "sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==",
"license": "MIT",
"dependencies": {
"@taplo/lib": "^0.5.0"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
},
"peerDependencies": {
"prettier": "^3.0.3"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
@@ -7777,6 +7760,50 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"src/common/prettier/plugins/scala/prettier-plugin-scala": {
"name": "@simochee/prettier-plugin-scala",
"version": "0.1.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@simochee/scala-parser": "file:../scala-parser"
},
"devDependencies": {
"@tsconfig/node-ts": "^23.6.1",
"@tsconfig/node24": "^24.0.1",
"@types/node": "^22.10.2",
"prettier": "^3.4.2",
"tsup": "^8.5.0",
"typescript": "^5.7.2",
"vitest": "^2.1.9"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"src/common/prettier/plugins/scala/scala-parser": {
"name": "@simochee/scala-parser",
"version": "0.1.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"chevrotain": "^11.0.3"
},
"devDependencies": {
"@tsconfig/node-ts": "^23.6.1",
"@tsconfig/node24": "^24.0.1",
"@types/node": "^22.10.2",
"tsup": "^8.5.0",
"typescript": "^5.7.2",
"vitest": "^2.1.9"
},
"engines": {
"node": ">=20.0.0"
}
}
}
}

View File

@@ -41,27 +41,29 @@
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.2",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@prettier/plugin-xml": "^3.4.2",
"@reteps/dockerfmt": "^0.3.6",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"groovy-beautify": "^0.0.17",
"hsl-matcher": "^1.2.4",
"java-parser": "^3.0.1",
"jinx-rust": "^0.1.6",
"jsox": "^1.2.123",
"lezer": "^0.13.5",
"linguist-languages": "^9.0.0",
"node-sql-parser": "^5.3.12",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"prettier": "^3.6.2",
"prettier-plugin-toml": "^2.0.6",
"remarkable": "^2.0.1",
"sass": "^1.92.1",
"sh-syntax": "^0.5.8",

Binary file not shown.

View File

@@ -1,12 +1,43 @@
/**
* Go Prettier Plugin for Vite + Vue3 Environment
* Go Prettier Plugin - Universal Implementation
* WebAssembly-based Go code formatter for Prettier
* Supports both Node.js and Browser environments
*/
let initializePromise = null;
// Load WASM file from public directory
const loadWasm = async () => {
// Environment detection
const isNode = () => {
return typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null;
};
const isBrowser = () => {
return typeof window !== 'undefined' &&
typeof document !== 'undefined';
};
// Node.js WASM loading
const loadWasmNode = async () => {
try {
const fs = await import('fs');
const path = await import('path');
const { fileURLToPath } = await import('url');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const wasmPath = path.join(__dirname, 'go.wasm');
return fs.readFileSync(wasmPath);
} catch (error) {
console.error('Node.js WASM loading failed:', error);
throw error;
}
};
// Browser WASM loading
const loadWasmBrowser = async () => {
try {
const response = await fetch('/go.wasm');
if (!response.ok) {
@@ -14,58 +45,143 @@ const loadWasm = async () => {
}
return await response.arrayBuffer();
} catch (error) {
console.error('WASM loading failed:', error);
console.error('Browser WASM loading failed:', error);
throw error;
}
};
// Initialize Go runtime
const initGoRuntime = async () => {
// Node.js Go runtime initialization
const initGoRuntimeNode = async () => {
if (globalThis.Go) return;
// Auto-load wasm_exec.js if not available
try {
// Dynamic import of wasm_exec.js for Node.js
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
const path = await import('path');
const { fileURLToPath } = await import('url');
const script = document.createElement('script');
script.src = '/wasm_exec.js';
document.head.appendChild(script);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load wasm_exec.js
const wasmExecPath = path.join(__dirname, 'wasm_exec.js');
require(wasmExecPath);
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load wasm_exec.js'));
setTimeout(() => reject(new Error('wasm_exec.js loading timeout')), 5000);
});
if (!globalThis.Go) {
throw new Error('Go WASM runtime is not available after loading wasm_exec.js');
throw new Error('Go WASM runtime not available after loading wasm_exec.js');
}
} catch (error) {
console.error('Failed to load wasm_exec.js:', error);
console.error('Node.js Go runtime initialization failed:', error);
throw error;
}
};
// Browser Go runtime initialization
const initGoRuntimeBrowser = async () => {
// 总是重新初始化,因为可能存在版本不兼容问题
try {
// 移除旧的 Go 运行时
delete globalThis.Go;
// 动态导入本地的 wasm_exec.js 内容
const wasmExecResponse = await fetch('/wasm_exec.js');
if (!wasmExecResponse.ok) {
throw new Error(`Failed to fetch wasm_exec.js: ${wasmExecResponse.status}`);
}
const wasmExecCode = await wasmExecResponse.text();
// 在全局作用域中执行 wasm_exec.js 代码
const script = document.createElement('script');
script.textContent = wasmExecCode;
document.head.appendChild(script);
// 等待一小段时间确保脚本执行完成
await new Promise(resolve => setTimeout(resolve, 100));
if (!globalThis.Go) {
throw new Error('Go WASM runtime not available after executing wasm_exec.js');
}
console.log('Go runtime initialized successfully');
} catch (error) {
console.error('Browser Go runtime initialization failed:', error);
throw error;
}
};
// Universal initialization
const initialize = async () => {
if (initializePromise) return initializePromise;
initializePromise = (async () => {
await initGoRuntime();
let wasmBuffer;
console.log('Starting Go WASM initialization...');
// Environment-specific initialization
if (isNode()) {
console.log('Initializing for Node.js environment');
await initGoRuntimeNode();
wasmBuffer = await loadWasmNode();
} else if (isBrowser()) {
console.log('Initializing for Browser environment');
await initGoRuntimeBrowser();
wasmBuffer = await loadWasmBrowser();
} else {
throw new Error('Unsupported environment: neither Node.js nor Browser detected');
}
console.log('Creating Go instance...');
const go = new globalThis.Go();
const wasmBuffer = await loadWasm();
const {instance} = await WebAssembly.instantiate(wasmBuffer, go.importObject);
// Run Go program
// 详细检查 importObject
console.log('Go import object keys:', Object.keys(go.importObject));
if (go.importObject.gojs) {
console.log('gojs import keys:', Object.keys(go.importObject.gojs));
console.log('scheduleTimeoutEvent type:', typeof go.importObject.gojs['runtime.scheduleTimeoutEvent']);
}
console.log('Instantiating WebAssembly module...');
try {
const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject);
console.log('WebAssembly instantiation successful');
console.log('Running Go program...');
// Run Go program (don't await as it's a long-running service)
go.run(instance).catch(err => {
console.error('Go WASM program exit error:', err);
});
// Wait for initialization to complete
await new Promise(resolve => setTimeout(resolve, 200));
// Check if formatGo function is available
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function not available');
} catch (instantiateError) {
console.error('WebAssembly instantiation failed:', instantiateError);
console.error('Error details:', {
message: instantiateError.message,
name: instantiateError.name,
stack: instantiateError.stack
});
throw instantiateError;
}
// Wait for Go program to initialize and expose formatGo function
console.log('Waiting for formatGo function to be available...');
let retries = 0;
const maxRetries = 20; // 增加重试次数
while (typeof globalThis.formatGo !== 'function' && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 200)); // 增加等待时间
retries++;
if (retries % 5 === 0) {
console.log(`Waiting for formatGo function... (${retries}/${maxRetries})`);
}
}
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function not available after 20 retries');
}
console.log('Go WASM initialization completed successfully');
})();
return initializePromise;
@@ -74,14 +190,14 @@ const initialize = async () => {
export const languages = [
{
name: "Go",
parsers: ["go"],
parsers: ["go-format"],
extensions: [".go"],
vscodeLanguageIds: ["go"],
},
];
export const parsers = {
go: {
"go-format": {
parse: (text) => text,
astFormat: "go-format",
locStart: (node) => 0,
@@ -91,23 +207,35 @@ export const parsers = {
export const printers = {
"go-format": {
print: async (path) => {
await initialize();
print: (path) => {
const text = path.getValue();
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function missing');
// 如果 formatGo 函数不可用,尝试初始化
initialize().then(() => {
// 初始化完成后formatGo 应该可用
}).catch(err => {
console.error('Go WASM initialization failed:', err);
});
// 如果还是不可用,返回原始文本
return text;
}
try {
return globalThis.formatGo(text);
} catch (error) {
throw new Error(`Go formatting failed: ${error.message}`);
console.error('Go formatting failed:', error);
// 返回原始文本而不是抛出错误
return text;
}
},
},
};
// Export initialize function for manual initialization
export { initialize };
// Default export for Prettier plugin compatibility
export default {
languages,

Binary file not shown.

View File

@@ -2,216 +2,20 @@
// Package main implements a WebAssembly module that provides Go code formatting
// functionality for the Prettier plugin. This package exposes the formatGo function
// to JavaScript, enabling web-based Go code formatting with better error tolerance.
// to JavaScript, enabling web-based Go code formatting using Go's built-in format package.
//
// The module is designed to be compiled to WebAssembly and loaded in Node.js
// environments as part of the go-prettier-format plugin.
package main
import (
"bytes"
"fmt"
"go/format"
"go/parser"
"go/token"
"strings"
"syscall/js"
)
// formatGoCode attempts to format Go source code with better error tolerance
func formatGoCode(src string) (string, error) {
// Trim input but preserve leading/trailing newlines structure
trimmed := strings.TrimSpace(src)
if trimmed == "" {
return src, nil
}
// First try the standard format.Source for complete, valid code
if formatted, err := format.Source([]byte(src)); err == nil {
return string(formatted), nil
}
// Create a new file set for parsing
fset := token.NewFileSet()
// Strategy 1: Try as complete Go file
if parsed, err := parser.ParseFile(fset, "", src, parser.ParseComments); err == nil {
return formatASTNode(fset, parsed)
}
// Strategy 2: Try wrapping as package-level declarations
packageWrapped := fmt.Sprintf("package main\n\n%s", trimmed)
if parsed, err := parser.ParseFile(fset, "", packageWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractPackageContent(formatted), nil
}
}
// Strategy 3: Try wrapping in main function
funcWrapped := fmt.Sprintf("package main\n\nfunc main() {\n%s\n}", indentLines(trimmed, "\t"))
if parsed, err := parser.ParseFile(fset, "", funcWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractFunctionBody(formatted), nil
}
}
// Strategy 4: Try wrapping in anonymous function
anonWrapped := fmt.Sprintf("package main\n\nvar _ = func() {\n%s\n}", indentLines(trimmed, "\t"))
if parsed, err := parser.ParseFile(fset, "", anonWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractFunctionBody(formatted), nil
}
}
// Strategy 5: Try line-by-line formatting for complex cases
return formatLineByLine(trimmed, fset)
}
// formatASTNode formats an AST node using the standard formatter
func formatASTNode(fset *token.FileSet, node interface{}) (string, error) {
var buf bytes.Buffer
if err := format.Node(&buf, fset, node); err != nil {
return "", err
}
return buf.String(), nil
}
// extractPackageContent extracts content after package declaration
func extractPackageContent(formatted string) string {
lines := strings.Split(formatted, "\n")
var contentLines []string
skipNext := false
for _, line := range lines {
if strings.HasPrefix(line, "package ") {
skipNext = true
continue
}
if skipNext && strings.TrimSpace(line) == "" {
skipNext = false
continue
}
if !skipNext {
contentLines = append(contentLines, line)
}
}
return strings.Join(contentLines, "\n")
}
// extractFunctionBody extracts content from within a function body
func extractFunctionBody(formatted string) string {
lines := strings.Split(formatted, "\n")
var bodyLines []string
inFunction := false
braceCount := 0
for _, line := range lines {
if strings.Contains(line, "func ") && strings.Contains(line, "{") {
inFunction = true
braceCount = 1
continue
}
if inFunction {
// Count braces to know when function ends
braceCount += strings.Count(line, "{")
braceCount -= strings.Count(line, "}")
if braceCount == 0 {
break
}
// Remove one level of indentation and add the line
if strings.HasPrefix(line, "\t") {
bodyLines = append(bodyLines, line[1:])
} else {
bodyLines = append(bodyLines, line)
}
}
}
// Remove empty lines from start and end
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[0]) == "" {
bodyLines = bodyLines[1:]
}
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[len(bodyLines)-1]) == "" {
bodyLines = bodyLines[:len(bodyLines)-1]
}
return strings.Join(bodyLines, "\n")
}
// indentLines adds indentation to each non-empty line
func indentLines(text, indent string) string {
lines := strings.Split(text, "\n")
var indentedLines []string
for _, line := range lines {
if strings.TrimSpace(line) == "" {
indentedLines = append(indentedLines, "")
} else {
indentedLines = append(indentedLines, indent+line)
}
}
return strings.Join(indentedLines, "\n")
}
// formatLineByLine attempts to format each statement individually
func formatLineByLine(src string, fset *token.FileSet) (string, error) {
lines := strings.Split(src, "\n")
var formattedLines []string
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
formattedLines = append(formattedLines, "")
continue
}
// Try different wrapping strategies for individual lines
attempts := []string{
fmt.Sprintf("package main\n\nfunc main() {\n\t%s\n}", trimmedLine),
fmt.Sprintf("package main\n\n%s", trimmedLine),
fmt.Sprintf("package main\n\nvar _ = %s", trimmedLine),
}
formatted := trimmedLine // fallback
for _, attempt := range attempts {
if parsed, err := parser.ParseFile(fset, "", attempt, parser.ParseComments); err == nil {
if result, err := formatASTNode(fset, parsed); err == nil {
if extracted := extractSingleStatement(result, trimmedLine); extracted != "" {
formatted = extracted
break
}
}
}
}
formattedLines = append(formattedLines, formatted)
}
return strings.Join(formattedLines, "\n"), nil
}
// extractSingleStatement tries to extract a single formatted statement
func extractSingleStatement(formatted, original string) string {
lines := strings.Split(formatted, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "package ") &&
!strings.HasPrefix(trimmed, "func ") && !strings.HasPrefix(trimmed, "var _ =") &&
trimmed != "{" && trimmed != "}" {
// Remove leading tabs/spaces to normalize indentation
return strings.TrimLeft(line, " \t")
}
}
return original
}
// formatGo is a JavaScript-callable function that formats Go source code.
// It attempts multiple strategies to format code, including handling incomplete
// or syntactically invalid code fragments.
// It wraps the standard library's go/format.Source function to be accessible
// from JavaScript environments through WebAssembly.
//
// Parameters:
// - this: The JavaScript 'this' context (unused)
@@ -219,35 +23,42 @@ func extractSingleStatement(formatted, original string) string {
//
// Returns:
// - js.Value: The formatted Go source code as a JavaScript string value
// - If formatting fails completely, returns the original code unchanged
// - If formatting fails due to syntax errors, returns the original code unchanged
// - If no arguments are provided, returns js.Null() and logs an error
//
// The function handles syntax errors gracefully by returning the original code
// and logging error details to the JavaScript console for debugging purposes.
func formatGo(this js.Value, i []js.Value) interface{} {
if len(i) == 0 {
js.Global().Get("console").Call("error", "formatGo: missing code argument")
return js.Null()
}
code := i[0].String()
if strings.TrimSpace(code) == "" {
formatted, err := format.Source([]byte(code))
if err != nil {
// In case of a syntax error in the Go code, go/format returns an error.
// Prettier expects the original text to be returned in case of an error.
// We also log the error to the console for debugging purposes.
js.Global().Get("console").Call("error", "Error formatting Go code:", err.Error())
return js.ValueOf(code)
}
formatted, err := formatGoCode(code)
if err != nil {
js.Global().Get("console").Call("warn", "Go formatting had issues:", err.Error())
return js.ValueOf(code) // Return original code if all attempts fail
}
return js.ValueOf(formatted)
return js.ValueOf(string(formatted))
}
// main initializes the WebAssembly module and exposes the formatGo function
// to the JavaScript global scope.
// to the JavaScript global scope. The function sets up a blocking channel
// to prevent the WASM module from exiting, allowing it to serve as a
// long-running service for formatting operations.
//
// The exposed formatGo function can be called from JavaScript as:
//
// global.formatGo(sourceCode)
func main() {
// Create a channel to keep the Go program running
// Create a channel to keep the Go program running.
// This is necessary because the WASM module would exit otherwise.
c := make(chan struct{}, 0)
// Expose the formatGo function to the JavaScript global scope
// Expose the formatGo function to the JavaScript global scope.
js.Global().Set("formatGo", js.FuncOf(formatGo))
// Block forever

View File

@@ -0,0 +1,561 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@@ -0,0 +1,67 @@
/**
* Prettier Plugin for Groovy/Jenkins file formatting
*
* This plugin provides support for formatting Groovy and Jenkins files using the groovy-beautify library.
* It supports .groovy files and Jenkins-related files like Jenkinsfile.
*/
import type { Plugin, Parser, Printer } from 'prettier';
import groovyBeautify from 'groovy-beautify';
const parserName = 'groovy';
// 语言配置
const languages = [
{
name: 'Groovy',
aliases: ['groovy'],
parsers: [parserName],
filenames: ['jenkinsfile', 'Jenkinsfile'],
extensions: ['.jenkinsfile', '.Jenkinsfile', '.groovy'],
aceMode: 'groovy',
tmScope: 'source.groovy',
linguistLanguageId: 142,
vscodeLanguageIds: ['groovy']
},
];
// 解析器配置
const groovyParser: Parser<string> = {
astFormat: parserName,
parse: (text: string) => text,
locStart: () => 0,
locEnd: (node: string) => node.length,
};
// 打印器配置
const groovyPrinter: Printer<string> = {
print: (path, options) => {
try {
return groovyBeautify(path.node, {
width: options.printWidth || 80,
}).trim();
} catch (error) {
return path.node;
}
},
};
const options = {
};
// 插件对象
const groovyPlugin: Plugin = {
languages,
parsers: {
[parserName]: groovyParser,
},
printers: {
[parserName]: groovyPrinter,
},
options,
};
export default groovyPlugin;
export { languages };
export const parsers = groovyPlugin.parsers;
export const printers = groovyPlugin.printers;

View File

@@ -0,0 +1,391 @@
/**
* PowerShell AST 节点定义
* 定义抽象语法树的各种节点类型
*/
import { Token } from './lexer';
export interface ASTNode {
type: string;
start: number;
end: number;
line: number;
column: number;
}
export interface ScriptBlockAst extends ASTNode {
type: 'ScriptBlock';
statements: StatementAst[];
}
export interface StatementAst extends ASTNode {
type: string;
}
export interface ExpressionAst extends ASTNode {
type: string;
}
// 管道表达式
export interface PipelineAst extends StatementAst {
type: 'Pipeline';
elements: PipelineElementAst[];
}
export interface PipelineElementAst extends ASTNode {
type: 'PipelineElement';
expression: ExpressionAst;
}
// 命令表达式
export interface CommandAst extends ExpressionAst {
type: 'Command';
commandName: string;
parameters: ParameterAst[];
arguments: ExpressionAst[];
}
export interface ParameterAst extends ASTNode {
type: 'Parameter';
name: string;
value?: ExpressionAst;
}
// 赋值表达式
export interface AssignmentAst extends StatementAst {
type: 'Assignment';
left: ExpressionAst;
operator: string;
right: ExpressionAst;
}
// 变量表达式
export interface VariableAst extends ExpressionAst {
type: 'Variable';
name: string;
}
// 字面量表达式
export interface LiteralAst extends ExpressionAst {
type: 'Literal';
value: any;
literalType: 'String' | 'Number' | 'Boolean' | 'Null';
}
// 数组表达式
export interface ArrayAst extends ExpressionAst {
type: 'Array';
elements: ExpressionAst[];
}
// 哈希表表达式
export interface HashtableAst extends ExpressionAst {
type: 'Hashtable';
entries: HashtableEntryAst[];
}
export interface HashtableEntryAst extends ASTNode {
type: 'HashtableEntry';
key: ExpressionAst;
value: ExpressionAst;
}
// 函数定义
export interface FunctionDefinitionAst extends StatementAst {
type: 'FunctionDefinition';
name: string;
parameters: ParameterAst[];
body: ScriptBlockAst;
}
// 控制流结构
export interface IfStatementAst extends StatementAst {
type: 'IfStatement';
condition: ExpressionAst;
ifBody: ScriptBlockAst;
elseIfClauses: ElseIfClauseAst[];
elseBody?: ScriptBlockAst;
}
export interface ElseIfClauseAst extends ASTNode {
type: 'ElseIfClause';
condition: ExpressionAst;
body: ScriptBlockAst;
}
export interface WhileStatementAst extends StatementAst {
type: 'WhileStatement';
condition: ExpressionAst;
body: ScriptBlockAst;
}
export interface ForStatementAst extends StatementAst {
type: 'ForStatement';
initializer?: ExpressionAst;
condition?: ExpressionAst;
iterator?: ExpressionAst;
body: ScriptBlockAst;
}
export interface ForEachStatementAst extends StatementAst {
type: 'ForEachStatement';
variable: VariableAst;
iterable: ExpressionAst;
body: ScriptBlockAst;
}
export interface SwitchStatementAst extends StatementAst {
type: 'SwitchStatement';
value: ExpressionAst;
clauses: SwitchClauseAst[];
}
export interface SwitchClauseAst extends ASTNode {
type: 'SwitchClause';
pattern: ExpressionAst;
body: ScriptBlockAst;
}
export interface TryStatementAst extends StatementAst {
type: 'TryStatement';
body: ScriptBlockAst;
catchClauses: CatchClauseAst[];
finallyClause?: FinallyClauseAst;
}
export interface CatchClauseAst extends ASTNode {
type: 'CatchClause';
exceptionType?: string;
body: ScriptBlockAst;
}
export interface FinallyClauseAst extends ASTNode {
type: 'FinallyClause';
body: ScriptBlockAst;
}
// 二元操作表达式
export interface BinaryExpressionAst extends ExpressionAst {
type: 'BinaryExpression';
left: ExpressionAst;
operator: string;
right: ExpressionAst;
}
// 一元操作表达式
export interface UnaryExpressionAst extends ExpressionAst {
type: 'UnaryExpression';
operator: string;
operand: ExpressionAst;
}
// 括号表达式
export interface ParenthesizedExpressionAst extends ExpressionAst {
type: 'ParenthesizedExpression';
expression: ExpressionAst;
}
// 方法调用表达式
export interface MethodCallAst extends ExpressionAst {
type: 'MethodCall';
object: ExpressionAst;
methodName: string;
arguments: ExpressionAst[];
}
// 属性访问表达式
export interface PropertyAccessAst extends ExpressionAst {
type: 'PropertyAccess';
object: ExpressionAst;
propertyName: string;
}
// 索引访问表达式
export interface IndexAccessAst extends ExpressionAst {
type: 'IndexAccess';
object: ExpressionAst;
index: ExpressionAst;
}
// 注释节点
export interface CommentAst extends ASTNode {
type: 'Comment';
text: string;
isMultiline: boolean;
}
// 空白节点
export interface WhitespaceAst extends ASTNode {
type: 'Whitespace';
text: string;
}
// 工厂函数用于创建AST节点
export class ASTNodeFactory {
static createScriptBlock(statements: StatementAst[], start: number, end: number, line: number, column: number): ScriptBlockAst {
return {
type: 'ScriptBlock',
statements,
start,
end,
line,
column
};
}
static createPipeline(elements: PipelineElementAst[], start: number, end: number, line: number, column: number): PipelineAst {
return {
type: 'Pipeline',
elements,
start,
end,
line,
column
};
}
static createCommand(commandName: string, parameters: ParameterAst[], args: ExpressionAst[], start: number, end: number, line: number, column: number): CommandAst {
return {
type: 'Command',
commandName,
parameters,
arguments: args,
start,
end,
line,
column
};
}
static createAssignment(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): AssignmentAst {
return {
type: 'Assignment',
left,
operator,
right,
start,
end,
line,
column
};
}
static createVariable(name: string, start: number, end: number, line: number, column: number): VariableAst {
return {
type: 'Variable',
name,
start,
end,
line,
column
};
}
static createLiteral(value: any, literalType: 'String' | 'Number' | 'Boolean' | 'Null', start: number, end: number, line: number, column: number): LiteralAst {
return {
type: 'Literal',
value,
literalType,
start,
end,
line,
column
};
}
static createBinaryExpression(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): BinaryExpressionAst {
return {
type: 'BinaryExpression',
left,
operator,
right,
start,
end,
line,
column
};
}
static createIfStatement(condition: ExpressionAst, ifBody: ScriptBlockAst, elseIfClauses: ElseIfClauseAst[], elseBody: ScriptBlockAst | undefined, start: number, end: number, line: number, column: number): IfStatementAst {
return {
type: 'IfStatement',
condition,
ifBody,
elseIfClauses,
elseBody,
start,
end,
line,
column
};
}
static createFunctionDefinition(name: string, parameters: ParameterAst[], body: ScriptBlockAst, start: number, end: number, line: number, column: number): FunctionDefinitionAst {
return {
type: 'FunctionDefinition',
name,
parameters,
body,
start,
end,
line,
column
};
}
static createComment(text: string, isMultiline: boolean, start: number, end: number, line: number, column: number): CommentAst {
return {
type: 'Comment',
text,
isMultiline,
start,
end,
line,
column
};
}
}
// AST访问者模式接口
export interface ASTVisitor<T> {
visitScriptBlock(node: ScriptBlockAst): T;
visitPipeline(node: PipelineAst): T;
visitCommand(node: CommandAst): T;
visitAssignment(node: AssignmentAst): T;
visitVariable(node: VariableAst): T;
visitLiteral(node: LiteralAst): T;
visitBinaryExpression(node: BinaryExpressionAst): T;
visitIfStatement(node: IfStatementAst): T;
visitFunctionDefinition(node: FunctionDefinitionAst): T;
visitComment(node: CommentAst): T;
}
// AST遍历工具类
export class ASTTraverser {
static traverse<T>(node: ASTNode, visitor: Partial<ASTVisitor<T>>): T | undefined {
switch (node.type) {
case 'ScriptBlock':
return visitor.visitScriptBlock?.(node as ScriptBlockAst);
case 'Pipeline':
return visitor.visitPipeline?.(node as PipelineAst);
case 'Command':
return visitor.visitCommand?.(node as CommandAst);
case 'Assignment':
return visitor.visitAssignment?.(node as AssignmentAst);
case 'Variable':
return visitor.visitVariable?.(node as VariableAst);
case 'Literal':
return visitor.visitLiteral?.(node as LiteralAst);
case 'BinaryExpression':
return visitor.visitBinaryExpression?.(node as BinaryExpressionAst);
case 'IfStatement':
return visitor.visitIfStatement?.(node as IfStatementAst);
case 'FunctionDefinition':
return visitor.visitFunctionDefinition?.(node as FunctionDefinitionAst);
case 'Comment':
return visitor.visitComment?.(node as CommentAst);
default:
return undefined;
}
}
}

View File

@@ -0,0 +1,566 @@
/**
* PowerShell 代码生成器
* 遍历AST并根据格式化规则生成格式化的PowerShell代码
*/
import {
ASTNode,
ScriptBlockAst,
StatementAst,
ExpressionAst,
PipelineAst,
CommandAst,
AssignmentAst,
VariableAst,
LiteralAst,
BinaryExpressionAst,
IfStatementAst,
FunctionDefinitionAst,
ParameterAst,
CommentAst,
PipelineElementAst,
ElseIfClauseAst,
ASTTraverser
} from './ast';
import { FormatterRules, FormatterOptions } from './formatter-rules';
export class PowerShellCodeGenerator {
private rules: FormatterRules;
private indentLevel: number = 0;
private output: string[] = [];
private currentLineLength: number = 0;
private needsNewline: boolean = false;
private lastWasComment: boolean = false;
constructor(options: Partial<FormatterOptions> = {}) {
this.rules = new FormatterRules(options);
}
/**
* 生成格式化的PowerShell代码
*/
public generate(ast: ScriptBlockAst, comments: CommentAst[] = []): string {
this.output = [];
this.indentLevel = 0;
this.currentLineLength = 0;
this.needsNewline = false;
this.lastWasComment = false;
// 首先处理文档开头的注释
this.generateLeadingComments(comments);
// 生成主体代码
this.generateScriptBlock(ast);
// 处理文档末尾
this.handleFinalNewline();
const result = this.output.join('');
return this.postProcess(result);
}
private generateScriptBlock(node: ScriptBlockAst): void {
for (let i = 0; i < node.statements.length; i++) {
const statement = node.statements[i];
const nextStatement = i < node.statements.length - 1 ? node.statements[i + 1] : null;
this.generateStatement(statement);
// 在语句之间添加适当的空行
if (nextStatement) {
this.addStatementSeparation(statement, nextStatement);
}
}
}
private generateStatement(statement: StatementAst): void {
switch (statement.type) {
case 'Pipeline':
this.generatePipeline(statement as PipelineAst);
break;
case 'Assignment':
this.generateAssignment(statement as AssignmentAst);
break;
case 'IfStatement':
this.generateIfStatement(statement as IfStatementAst);
break;
case 'FunctionDefinition':
this.generateFunctionDefinition(statement as FunctionDefinitionAst);
break;
case 'RawText':
// 处理解析失败时的原始文本
this.append((statement as any).value);
return; // 不需要添加额外的换行
default:
this.append(`/* Unsupported statement type: ${statement.type} */`);
break;
}
this.ensureNewline();
}
private generatePipeline(pipeline: PipelineAst): void {
if (!this.rules.formatPipelines) {
// 简单连接所有元素
for (let i = 0; i < pipeline.elements.length; i++) {
if (i > 0) {
this.append(' | ');
}
this.generatePipelineElement(pipeline.elements[i]);
}
return;
}
const style = this.rules.getPipelineStyle(pipeline.elements.length);
if (style === 'multiline') {
this.generateMultilinePipeline(pipeline);
} else {
this.generateOnelinePipeline(pipeline);
}
}
private generateOnelinePipeline(pipeline: PipelineAst): void {
for (let i = 0; i < pipeline.elements.length; i++) {
if (i > 0) {
this.append(' | ');
}
this.generatePipelineElement(pipeline.elements[i]);
}
}
private generateMultilinePipeline(pipeline: PipelineAst): void {
for (let i = 0; i < pipeline.elements.length; i++) {
if (i > 0) {
this.appendLine(' |');
this.appendIndent();
}
this.generatePipelineElement(pipeline.elements[i]);
}
}
private generatePipelineElement(element: PipelineElementAst): void {
this.generateExpression(element.expression);
}
private generateExpression(expression: ExpressionAst): void {
switch (expression.type) {
case 'Command':
this.generateCommand(expression as CommandAst);
break;
case 'Variable':
this.generateVariable(expression as VariableAst);
break;
case 'Literal':
this.generateLiteral(expression as LiteralAst);
break;
case 'BinaryExpression':
this.generateBinaryExpression(expression as BinaryExpressionAst);
break;
case 'ParenthesizedExpression':
this.append('(');
this.generateExpression((expression as any).expression);
this.append(')');
break;
case 'Array':
this.generateArray(expression as any);
break;
case 'Hashtable':
this.generateHashtable(expression as any);
break;
case 'ScriptBlockExpression':
this.generateScriptBlockExpression(expression as any);
break;
default:
this.append(`/* Unsupported expression type: ${expression.type} */`);
break;
}
}
private generateCommand(command: CommandAst): void {
// 保持cmdlet名称的连字符不进行破坏性的格式化
let commandName = command.commandName;
// 只有在明确指定要改变大小写时才进行格式化
// 但绝对不能删除连字符
if (this.rules.shouldFormatCommandCase()) {
commandName = this.rules.formatCommandCase(commandName);
}
this.append(commandName);
// 生成参数
for (const param of command.parameters) {
this.append(' ');
this.generateParameter(param);
}
// 生成位置参数
for (const arg of command.arguments) {
this.append(' ');
this.generateExpression(arg);
}
}
private generateParameter(parameter: ParameterAst): void {
const paramName = this.rules.formatParameterCase(parameter.name);
this.append(paramName);
if (parameter.value) {
this.append(' ');
this.generateExpression(parameter.value);
}
}
private generateVariable(variable: VariableAst): void {
const formattedName = this.rules.formatVariableCase(variable.name);
this.append(formattedName);
}
private generateLiteral(literal: LiteralAst): void {
if (literal.literalType === 'String') {
const formattedString = this.rules.formatQuotes(literal.value as string);
this.append(formattedString);
} else {
this.append(String(literal.value));
}
}
private generateBinaryExpression(expression: BinaryExpressionAst): void {
this.generateExpression(expression.left);
// 根据PowerShell官方规范属性访问操作符绝对不能加空格
if (expression.operator === '.' ||
expression.operator === '::' ||
expression.operator === '[' ||
expression.operator === ']' ||
expression.operator === '@{') {
// 属性访问是PowerShell面向对象的核心必须保持紧凑
this.append(expression.operator);
} else {
// 使用格式化规则处理其他操作符
const formattedOperator = this.rules.formatOperatorSpacing(expression.operator);
this.append(formattedOperator);
}
this.generateExpression(expression.right);
}
private generateAssignment(assignment: AssignmentAst): void {
this.generateExpression(assignment.left);
const formattedOperator = this.rules.formatOperatorSpacing(assignment.operator);
this.append(formattedOperator);
this.generateExpression(assignment.right);
}
private generateIfStatement(ifStmt: IfStatementAst): void {
// if 条件
this.append('if ');
this.append(this.rules.formatParentheses(''));
this.append('(');
this.generateExpression(ifStmt.condition);
this.append(')');
// if 主体
this.append(this.rules.getBraceStart());
this.appendLine('');
this.indent();
this.generateScriptBlock(ifStmt.ifBody);
this.outdent();
this.appendIndent();
this.append('}');
// elseif 子句
for (const elseIfClause of ifStmt.elseIfClauses) {
this.generateElseIfClause(elseIfClause);
}
// else 子句
if (ifStmt.elseBody) {
this.append(' else');
this.append(this.rules.getBraceStart());
this.appendLine('');
this.indent();
this.generateScriptBlock(ifStmt.elseBody);
this.outdent();
this.appendIndent();
this.append('}');
}
}
private generateElseIfClause(elseIf: ElseIfClauseAst): void {
this.append(' elseif (');
this.generateExpression(elseIf.condition);
this.append(')');
this.append(this.rules.getBraceStart());
this.appendLine('');
this.indent();
this.generateScriptBlock(elseIf.body);
this.outdent();
this.appendIndent();
this.append('}');
}
private generateFunctionDefinition(func: FunctionDefinitionAst): void {
// 函数前的空行
if (this.rules.blankLinesAroundFunctions > 0) {
for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) {
this.appendLine('');
}
}
this.append('function ');
this.append(func.name);
// 参数列表
if (func.parameters.length > 0) {
this.append('(');
for (let i = 0; i < func.parameters.length; i++) {
if (i > 0) {
this.append(this.rules.formatComma());
}
this.generateParameter(func.parameters[i]);
}
this.append(')');
}
// 函数体
this.append(this.rules.getBraceStart());
this.appendLine('');
this.indent();
this.generateScriptBlock(func.body);
this.outdent();
this.appendIndent();
this.append('}');
// 函数后的空行
if (this.rules.blankLinesAroundFunctions > 0) {
for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) {
this.appendLine('');
}
}
}
private generateLeadingComments(comments: CommentAst[]): void {
const leadingComments = comments.filter(c => this.isLeadingComment(c));
for (const comment of leadingComments) {
this.generateComment(comment);
this.appendLine('');
}
}
private generateComment(comment: CommentAst): void {
if (!this.rules.formatComments) {
this.append(comment.text);
return;
}
if (comment.isMultiline) {
this.generateMultilineComment(comment.text);
} else {
this.generateSingleLineComment(comment.text);
}
this.lastWasComment = true;
}
private generateArray(arrayExpr: any): void {
this.append('@(');
if (arrayExpr.elements && arrayExpr.elements.length > 0) {
for (let i = 0; i < arrayExpr.elements.length; i++) {
if (i > 0) {
this.append(this.rules.formatComma());
}
this.generateExpression(arrayExpr.elements[i]);
}
}
this.append(')');
}
private generateHashtable(hashtableExpr: any): void {
this.append('@{');
if (hashtableExpr.entries && hashtableExpr.entries.length > 0) {
// 强制使用紧凑格式,避免换行问题
for (let i = 0; i < hashtableExpr.entries.length; i++) {
const entry = hashtableExpr.entries[i];
this.generateExpression(entry.key);
this.append('=');
this.generateExpression(entry.value);
// 如果不是最后一个条目,添加分号和空格
if (i < hashtableExpr.entries.length - 1) {
this.append('; ');
}
}
}
this.append('}');
}
private generateScriptBlockExpression(scriptBlockExpr: any): void {
this.append('{');
// 对原始内容应用基本的格式化规则
if (scriptBlockExpr.rawContent) {
const formattedContent = this.formatScriptBlockContent(scriptBlockExpr.rawContent);
this.append(formattedContent);
} else if (scriptBlockExpr.expression) {
// 兼容旧格式
this.generateExpression(scriptBlockExpr.expression);
}
this.append('}');
}
private formatScriptBlockContent(content: string): string {
if (!content || !content.trim()) {
return content;
}
// 应用PowerShell官方规范的格式化规则
let formatted = content.trim();
// 1. 保护所有属性访问操作符 - 这是最关键的
// 匹配所有形式的属性访问:$var.Property, $_.Property, $obj.Method.Property等
formatted = formatted.replace(/(\$[a-zA-Z_][a-zA-Z0-9_]*|\$_)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)/g, '$1.$2');
// 2. 保护方法调用中的点号
formatted = formatted.replace(/(\w)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, '$1.$2(');
// 3. 确保数字单位不被分离
formatted = formatted.replace(/(\d+)\s*(KB|MB|GB|TB|PB)/gi, '$1$2');
// 4. PowerShell比较和逻辑操作符需要前后空格
const powershellOps = [
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
'-like', '-notlike', '-match', '-notmatch',
'-contains', '-notcontains', '-in', '-notin',
'-is', '-isnot', '-as', '-and', '-or', '-not', '-xor'
];
for (const op of powershellOps) {
const regex = new RegExp(`\\s*${op.replace('-', '\\-')}\\s*`, 'gi');
formatted = formatted.replace(regex, ` ${op} `);
}
// 5. 清理多余空格,但保护属性访问
formatted = formatted.replace(/\s{2,}/g, ' ').trim();
// 6. 最终检查:确保没有属性访问被破坏
formatted = formatted.replace(/(\$\w+|\$_)\s+\.\s*/g, '$1.');
return formatted;
}
private generateSingleLineComment(text: string): void {
// 确保单行注释以 # 开头
const cleanText = text.startsWith('#') ? text : `# ${text}`;
this.append(cleanText);
}
private generateMultilineComment(text: string): void {
// 多行注释保持原格式
this.append(text);
}
private isLeadingComment(comment: CommentAst): boolean {
// 简单判断:如果注释在文档开头,就认为是前导注释
return comment.line <= 3;
}
private addStatementSeparation(current: StatementAst, next: StatementAst): void {
// 函数之间添加空行
if (current.type === 'FunctionDefinition' || next.type === 'FunctionDefinition') {
this.appendLine('');
}
// 控制结构前添加空行
if (next.type === 'IfStatement' && !this.lastWasComment) {
this.appendLine('');
}
}
private handleFinalNewline(): void {
if (this.rules.insertFinalNewline && this.output.length > 0) {
const lastLine = this.output[this.output.length - 1];
if (!lastLine.endsWith(this.rules.getNewline())) {
this.appendLine('');
}
}
}
private postProcess(code: string): string {
let result = code;
// 清理多余的空行
if (this.rules.maxConsecutiveEmptyLines >= 0) {
const maxEmpty = this.rules.maxConsecutiveEmptyLines;
const emptyLinePattern = new RegExp(`(${this.rules.getNewline()}){${maxEmpty + 2},}`, 'g');
const replacement = this.rules.getNewline().repeat(maxEmpty + 1);
result = result.replace(emptyLinePattern, replacement);
}
// 清理行尾空白
if (this.rules.trimTrailingWhitespace) {
result = result.replace(/ +$/gm, '');
}
return result;
}
// 辅助方法
private append(text: string): void {
this.output.push(text);
this.currentLineLength += text.length;
this.needsNewline = false;
this.lastWasComment = false;
}
private appendLine(text: string): void {
this.output.push(text + this.rules.getNewline());
this.currentLineLength = 0;
this.needsNewline = false;
this.lastWasComment = false;
}
private appendIndent(): void {
const indent = this.rules.getIndent(this.indentLevel);
this.append(indent);
}
private ensureNewline(): void {
if (!this.needsNewline) {
this.appendLine('');
this.needsNewline = true;
}
}
private indent(): void {
this.indentLevel++;
}
private outdent(): void {
this.indentLevel = Math.max(0, this.indentLevel - 1);
}
private shouldWrapLine(): boolean {
return this.currentLineLength > this.rules.printWidth;
}
}
/**
* 便捷函数格式化PowerShell AST
*/
export function formatPowerShellAST(
ast: ScriptBlockAst,
comments: CommentAst[] = [],
options: Partial<FormatterOptions> = {}
): string {
const generator = new PowerShellCodeGenerator(options);
return generator.generate(ast, comments);
}

View File

@@ -0,0 +1,440 @@
/**
* PowerShell 格式化规则引擎
* 定义各种可配置的代码格式化规则和策略
*/
export interface FormatterOptions {
// 基本格式化选项
indentSize: number; // 缩进大小
useTabsForIndentation: boolean; // 使用制表符还是空格
printWidth: number; // 行最大长度
endOfLine: 'lf' | 'crlf' | 'cr' | 'auto'; // 行尾符类型
// 空格和间距
spaceAroundOperators: boolean; // 操作符周围的空格
spaceAfterCommas: boolean; // 逗号后的空格
spaceAfterSemicolons: boolean; // 分号后的空格
spaceInsideParentheses: boolean; // 括号内的空格
spaceInsideBrackets: boolean; // 方括号内的空格
spaceInsideBraces: boolean; // 大括号内的空格
// 换行和空行
maxConsecutiveEmptyLines: number; // 最大连续空行数
insertFinalNewline: boolean; // 文件末尾插入换行符
trimTrailingWhitespace: boolean; // 删除行尾空白
blankLinesAroundFunctions: number; // 函数前后的空行数
blankLinesAroundClasses: number; // 类前后的空行数
blankLinesAroundIfStatements: boolean; // if语句前后的空行
// 括号和大括号
braceStyle: 'allman' | 'otbs' | 'stroustrup'; // 大括号风格
alwaysParenthesizeArrowFunctions: boolean; // 箭头函数总是用括号
// PowerShell特定选项
formatPipelines: boolean; // 格式化管道
pipelineStyle: 'oneline' | 'multiline' | 'auto'; // 管道风格
formatParameters: boolean; // 格式化参数
parameterAlignment: 'left' | 'right' | 'auto'; // 参数对齐方式
formatHashtables: boolean; // 格式化哈希表
hashtableStyle: 'compact' | 'expanded'; // 哈希表风格
formatArrays: boolean; // 格式化数组
arrayStyle: 'compact' | 'expanded'; // 数组风格
formatComments: boolean; // 格式化注释
commentAlignment: 'left' | 'preserve'; // 注释对齐方式
// 命名和大小写
preferredCommandCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 命令大小写
preferredParameterCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 参数大小写
preferredVariableCase: 'camelcase' | 'pascalcase' | 'preserve'; // 变量大小写
// 引号和字符串
quotestyle: 'single' | 'double' | 'preserve'; // 引号风格
escapeNonAscii: boolean; // 转义非ASCII字符
// 长度和换行
wrapLongLines: boolean; // 自动换行长行
wrapParameters: boolean; // 换行长参数列表
wrapArrays: boolean; // 换行长数组
wrapHashtables: boolean; // 换行长哈希表
}
export const DEFAULT_OPTIONS: FormatterOptions = {
// 基本选项
indentSize: 4,
useTabsForIndentation: false,
printWidth: 120,
endOfLine: 'auto',
// 空格设置
spaceAroundOperators: true,
spaceAfterCommas: true,
spaceAfterSemicolons: true,
spaceInsideParentheses: false,
spaceInsideBrackets: false,
spaceInsideBraces: true,
// 空行设置
maxConsecutiveEmptyLines: 2,
insertFinalNewline: true,
trimTrailingWhitespace: true,
blankLinesAroundFunctions: 1,
blankLinesAroundClasses: 1,
blankLinesAroundIfStatements: false,
// 括号风格
braceStyle: 'otbs', // One True Brace Style
alwaysParenthesizeArrowFunctions: false,
// PowerShell特定
formatPipelines: true,
pipelineStyle: 'auto',
formatParameters: true,
parameterAlignment: 'left',
formatHashtables: true,
hashtableStyle: 'compact',
formatArrays: true,
arrayStyle: 'compact',
formatComments: true,
commentAlignment: 'preserve',
// 命名约定
preferredCommandCase: 'pascalcase',
preferredParameterCase: 'preserve',
preferredVariableCase: 'preserve',
// 字符串设置
quotestyle: 'preserve',
escapeNonAscii: false,
// 长度处理
wrapLongLines: true,
wrapParameters: true,
wrapArrays: true,
wrapHashtables: true
};
/**
* 格式化规则类,包含各种格式化策略的实现
*/
export class FormatterRules {
private options: FormatterOptions;
constructor(options: Partial<FormatterOptions> = {}) {
this.options = { ...DEFAULT_OPTIONS, ...options };
}
/**
* 获取缩进字符串
*/
getIndent(level: number): string {
if (level <= 0) return '';
const indentChar = this.options.useTabsForIndentation ? '\t' : ' ';
const indentSize = this.options.useTabsForIndentation ? 1 : this.options.indentSize;
return indentChar.repeat(level * indentSize);
}
/**
* 获取换行符
*/
getNewline(): string {
switch (this.options.endOfLine) {
case 'lf': return '\n';
case 'crlf': return '\r\n';
case 'cr': return '\r';
case 'auto':
default:
// 在浏览器环境中默认使用 LF
return '\n';
}
}
/**
* 格式化操作符周围的空格
*/
formatOperatorSpacing(operator: string): string {
if (!this.options.spaceAroundOperators) {
return operator;
}
// PowerShell语法中绝对不能加空格的操作符官方规范
const noSpaceOperators = [
'.', '::', // 属性访问和静态成员访问 - 这是PowerShell面向对象的核心
'[', ']', // 数组索引和类型转换
'(', ')', '{', '}', // 括号
'@{', // 哈希表字面量开始
';', // 哈希表和语句分隔符
'-', // cmdlet连字符Get-ChildItem中的-
'::' // 静态成员访问
];
if (noSpaceOperators.includes(operator)) {
return operator;
}
// PowerShell比较操作符需要空格
const powershellOperators = ['-eq', '-ne', '-lt', '-le', '-gt', '-ge',
'-like', '-notlike', '-match', '-notmatch',
'-contains', '-notcontains', '-in', '-notin',
'-is', '-isnot', '-as', '-and', '-or', '-not', '-xor'];
if (powershellOperators.some(op => operator.toLowerCase() === op)) {
return ` ${operator} `;
}
// 算术和赋值操作符需要空格
const spaceOperators = ['=', '+=', '-=', '*=', '/=', '%=', '+', '*', '/', '%'];
if (spaceOperators.includes(operator)) {
return ` ${operator} `;
}
return operator;
}
/**
* 格式化逗号后的空格
*/
formatComma(): string {
return this.options.spaceAfterCommas ? ', ' : ',';
}
/**
* 格式化分号后的空格
*/
formatSemicolon(): string {
return this.options.spaceAfterSemicolons ? '; ' : ';';
}
/**
* 格式化括号内的空格
*/
formatParentheses(content: string): string {
if (this.options.spaceInsideParentheses) {
return `( ${content} )`;
}
return `(${content})`;
}
/**
* 格式化方括号内的空格
*/
formatBrackets(content: string): string {
if (this.options.spaceInsideBrackets) {
return `[ ${content} ]`;
}
return `[${content}]`;
}
/**
* 格式化大括号内的空格
*/
formatBraces(content: string): string {
if (this.options.spaceInsideBraces) {
return `{ ${content} }`;
}
return `{${content}}`;
}
/**
* 获取大括号的开始位置
*/
getBraceStart(): string {
switch (this.options.braceStyle) {
case 'allman':
return this.getNewline() + '{';
case 'stroustrup':
return this.getNewline() + '{';
case 'otbs':
default:
return ' {';
}
}
/**
* 格式化命令名的大小写
*/
formatCommandCase(command: string): string {
switch (this.options.preferredCommandCase) {
case 'lowercase':
return command.toLowerCase();
case 'uppercase':
return command.toUpperCase();
case 'pascalcase':
return this.toPascalCasePreservingHyphens(command);
case 'preserve':
default:
return command;
}
}
/**
* 检查是否应该格式化命令大小写
*/
shouldFormatCommandCase(): boolean {
return this.options.preferredCommandCase !== 'preserve';
}
/**
* 格式化参数名的大小写
*/
formatParameterCase(parameter: string): string {
switch (this.options.preferredParameterCase) {
case 'lowercase':
return parameter.toLowerCase();
case 'uppercase':
return parameter.toUpperCase();
case 'pascalcase':
return this.toPascalCase(parameter);
case 'preserve':
default:
return parameter;
}
}
/**
* 格式化变量名的大小写
*/
formatVariableCase(variable: string): string {
if (!variable.startsWith('$')) {
return variable;
}
const variableName = variable.substring(1);
let formattedName: string;
switch (this.options.preferredVariableCase) {
case 'camelcase':
formattedName = this.toCamelCase(variableName);
break;
case 'pascalcase':
formattedName = this.toPascalCase(variableName);
break;
case 'preserve':
default:
formattedName = variableName;
break;
}
return '$' + formattedName;
}
/**
* 格式化字符串引号
*/
formatQuotes(value: string): string {
if (this.options.quotestyle === 'preserve') {
return value;
}
const content = this.extractStringContent(value);
switch (this.options.quotestyle) {
case 'single':
return `'${content.replace(/'/g, "''")}'`;
case 'double':
return `"${content.replace(/"/g, '""')}"`;
default:
return value;
}
}
/**
* 检查是否需要换行
*/
shouldWrapLine(line: string): boolean {
return this.options.wrapLongLines && line.length > this.options.printWidth;
}
/**
* 获取管道样式
*/
getPipelineStyle(elementCount: number): 'oneline' | 'multiline' {
switch (this.options.pipelineStyle) {
case 'oneline':
return 'oneline';
case 'multiline':
return 'multiline';
case 'auto':
default:
return elementCount > 2 ? 'multiline' : 'oneline';
}
}
/**
* 获取哈希表样式
*/
getHashtableStyle(entryCount: number): 'compact' | 'expanded' {
if (this.options.hashtableStyle === 'compact') {
return 'compact';
}
if (this.options.hashtableStyle === 'expanded') {
return 'expanded';
}
// auto logic: 对于小型哈希表默认使用compact避免不必要的换行
return entryCount > 5 ? 'expanded' : 'compact';
}
/**
* 获取数组样式
*/
getArrayStyle(elementCount: number): 'compact' | 'expanded' {
if (this.options.arrayStyle === 'compact') {
return 'compact';
}
if (this.options.arrayStyle === 'expanded') {
return 'expanded';
}
// auto logic could be added here
return elementCount > 5 ? 'expanded' : 'compact';
}
// 辅助方法
private toPascalCase(str: string): string {
return str.split(/[-_\s]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
/**
* 转换为PascalCase但保留连字符专门用于PowerShell cmdlet
*/
private toPascalCasePreservingHyphens(str: string): string {
return str.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join('-');
}
private toCamelCase(str: string): string {
const pascalCase = this.toPascalCase(str);
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
}
private extractStringContent(str: string): string {
if ((str.startsWith('"') && str.endsWith('"')) ||
(str.startsWith("'") && str.endsWith("'"))) {
return str.slice(1, -1);
}
return str;
}
// Getter methods for options
get indentSize(): number { return this.options.indentSize; }
get printWidth(): number { return this.options.printWidth; }
get maxConsecutiveEmptyLines(): number { return this.options.maxConsecutiveEmptyLines; }
get insertFinalNewline(): boolean { return this.options.insertFinalNewline; }
get trimTrailingWhitespace(): boolean { return this.options.trimTrailingWhitespace; }
get blankLinesAroundFunctions(): number { return this.options.blankLinesAroundFunctions; }
get formatPipelines(): boolean { return this.options.formatPipelines; }
get formatParameters(): boolean { return this.options.formatParameters; }
get formatHashtables(): boolean { return this.options.formatHashtables; }
get formatArrays(): boolean { return this.options.formatArrays; }
get formatComments(): boolean { return this.options.formatComments; }
/**
* 创建规则的副本,可以重写部分选项
*/
withOptions(overrides: Partial<FormatterOptions>): FormatterRules {
return new FormatterRules({ ...this.options, ...overrides });
}
}

View File

@@ -0,0 +1,208 @@
/**
* Prettier Plugin for PowerShell file formatting - Modular Version
*
* This plugin provides support for formatting PowerShell files (.ps1, .psm1, .psd1)
* using a modular architecture with lexer, parser, AST, and code generator.
*/
import type { Plugin, Parser, Printer, AstPath, Doc } from 'prettier';
import { PowerShellLexer } from './lexer';
import { PowerShellParser } from './parser';
import { ScriptBlockAst, CommentAst } from './ast';
import { formatPowerShellAST } from './code-generator';
import { FormatterOptions, DEFAULT_OPTIONS } from './formatter-rules';
// PowerShell格式化结果接口
interface PowerShellParseResult {
ast: ScriptBlockAst;
comments: CommentAst[];
originalText: string;
}
const parserName = 'powershell';
// 语言配置
const languages = [
{
name: 'PowerShell',
aliases: ['powershell', 'pwsh', 'posh'],
parsers: [parserName],
extensions: ['.ps1', '.psm1', '.psd1'],
filenames: ['profile.ps1'],
tmScope: 'source.powershell',
aceMode: 'powershell',
linguistLanguageId: 295,
vscodeLanguageIds: ['powershell']
},
];
// 解析器配置
const powershellParser: Parser<PowerShellParseResult> = {
parse: parseCode,
astFormat: 'powershell',
locStart: (node: PowerShellParseResult) => 0,
locEnd: (node: PowerShellParseResult) => node.originalText.length,
};
/**
* 解析PowerShell代码
*/
async function parseCode(text: string, parsers?: any, options?: any): Promise<PowerShellParseResult> {
try {
// 词法分析
const lexer = new PowerShellLexer(text);
const tokens = lexer.tokenize();
// 语法分析
const parser = new PowerShellParser(tokens, text);
const ast = parser.parse();
const comments = parser.getComments();
return {
ast,
comments,
originalText: text
};
} catch (error) {
console.warn('PowerShell parsing failed, using fallback:', error);
// 解析失败时创建一个包含原始文本的简单AST
// 这样可以确保格式化失败时返回原始代码而不是空内容
return {
ast: {
type: 'ScriptBlock',
statements: [{
type: 'RawText',
value: text,
start: 0,
end: text.length,
line: 1,
column: 1
} as any],
start: 0,
end: text.length,
line: 1,
column: 1
},
comments: [],
originalText: text
};
}
}
/**
* PowerShell代码打印器
*/
const printPowerShell = (path: AstPath<PowerShellParseResult>, options: any): Doc => {
const parseResult = path.node;
try {
// 构建格式化选项 - 优先保持原有格式避免破坏PowerShell语法
const formatterOptions: Partial<FormatterOptions> = {
indentSize: options.tabWidth || DEFAULT_OPTIONS.indentSize,
useTabsForIndentation: options.useTabs || DEFAULT_OPTIONS.useTabsForIndentation,
printWidth: options.printWidth || DEFAULT_OPTIONS.printWidth,
spaceAroundOperators: true,
formatPipelines: true,
formatParameters: true,
formatHashtables: true,
hashtableStyle: 'compact', // 强制使用紧凑格式,避免不必要的换行
formatArrays: true,
arrayStyle: 'compact',
formatComments: true,
maxConsecutiveEmptyLines: 1,
insertFinalNewline: true,
trimTrailingWhitespace: true,
blankLinesAroundFunctions: 1,
braceStyle: 'otbs',
preferredCommandCase: 'preserve', // 保持原有命令大小写,不破坏语法
preferredParameterCase: 'preserve',
preferredVariableCase: 'preserve',
quotestyle: 'preserve',
wrapLongLines: true
};
// 使用新的模块化格式化器
const formattedCode = formatPowerShellAST(
parseResult.ast,
parseResult.comments,
formatterOptions
);
return formattedCode;
} catch (error) {
console.warn('PowerShell formatting failed, returning original code:', error);
return parseResult.originalText;
}
};
// 打印器配置
const powershellPrinter: Printer<PowerShellParseResult> = {
print: printPowerShell,
};
// 插件选项配置
const options = {
// PowerShell特定格式化选项
powershellBraceStyle: {
type: 'choice' as const,
category: 'PowerShell',
default: DEFAULT_OPTIONS.braceStyle,
description: 'PowerShell大括号样式',
choices: [
{ value: 'allman', description: 'Allman风格大括号另起一行' },
{ value: 'otbs', description: '1TBS风格大括号同行' },
{ value: 'stroustrup', description: 'Stroustrup风格' }
]
},
powershellCommandCase: {
type: 'choice' as const,
category: 'PowerShell',
default: DEFAULT_OPTIONS.preferredCommandCase,
description: 'PowerShell命令大小写风格',
choices: [
{ value: 'lowercase', description: '小写' },
{ value: 'uppercase', description: '大写' },
{ value: 'pascalcase', description: 'Pascal大小写' },
{ value: 'preserve', description: '保持原样' }
]
},
powershellPipelineStyle: {
type: 'choice' as const,
category: 'PowerShell',
default: DEFAULT_OPTIONS.pipelineStyle,
description: 'PowerShell管道样式',
choices: [
{ value: 'oneline', description: '单行' },
{ value: 'multiline', description: '多行' },
{ value: 'auto', description: '自动' }
]
},
powershellSpaceAroundOperators: {
type: 'boolean' as const,
category: 'PowerShell',
default: DEFAULT_OPTIONS.spaceAroundOperators,
description: '在操作符周围添加空格'
},
powershellMaxEmptyLines: {
type: 'int' as const,
category: 'PowerShell',
default: DEFAULT_OPTIONS.maxConsecutiveEmptyLines,
description: '最大连续空行数'
}
};
const powershellPlugin: Plugin = {
languages,
parsers: {
[parserName]: powershellParser,
},
printers: {
[parserName]: powershellPrinter,
},
options,
};
export default powershellPlugin;
export { languages };
export const parsers = powershellPlugin.parsers;
export const printers = powershellPlugin.printers;

View File

@@ -0,0 +1,722 @@
/**
* PowerShell 词法分析器 (Lexer)
* 将PowerShell代码分解为tokens用于后续的语法分析和格式化
*/
export enum TokenType {
// 字面量
STRING = 'STRING',
NUMBER = 'NUMBER',
VARIABLE = 'VARIABLE',
// 关键字
KEYWORD = 'KEYWORD',
FUNCTION = 'FUNCTION',
// 操作符
OPERATOR = 'OPERATOR',
ASSIGNMENT = 'ASSIGNMENT',
COMPARISON = 'COMPARISON',
LOGICAL = 'LOGICAL',
ARITHMETIC = 'ARITHMETIC',
// 分隔符
LEFT_PAREN = 'LEFT_PAREN',
RIGHT_PAREN = 'RIGHT_PAREN',
LEFT_BRACE = 'LEFT_BRACE',
RIGHT_BRACE = 'RIGHT_BRACE',
LEFT_BRACKET = 'LEFT_BRACKET',
RIGHT_BRACKET = 'RIGHT_BRACKET',
SEMICOLON = 'SEMICOLON',
COMMA = 'COMMA',
DOT = 'DOT',
PIPE = 'PIPE',
// 特殊
WHITESPACE = 'WHITESPACE',
NEWLINE = 'NEWLINE',
COMMENT = 'COMMENT',
MULTILINE_COMMENT = 'MULTILINE_COMMENT',
HERE_STRING = 'HERE_STRING',
// 控制结构
IF = 'IF',
ELSE = 'ELSE',
ELSEIF = 'ELSEIF',
WHILE = 'WHILE',
FOR = 'FOR',
FOREACH = 'FOREACH',
SWITCH = 'SWITCH',
TRY = 'TRY',
CATCH = 'CATCH',
FINALLY = 'FINALLY',
// 其他
IDENTIFIER = 'IDENTIFIER',
CMDLET = 'CMDLET',
PARAMETER = 'PARAMETER',
EOF = 'EOF',
UNKNOWN = 'UNKNOWN'
}
export interface Token {
type: TokenType;
value: string;
line: number;
column: number;
startIndex: number;
endIndex: number;
}
export class PowerShellLexer {
private code: string;
private position: number = 0;
private line: number = 1;
private column: number = 1;
private tokens: Token[] = [];
// PowerShell关键字
private readonly keywords = new Set([
'if', 'else', 'elseif', 'switch', 'while', 'for', 'foreach', 'do',
'try', 'catch', 'finally', 'throw', 'return', 'break', 'continue',
'function', 'filter', 'param', 'begin', 'process', 'end',
'class', 'enum', 'using', 'namespace', 'workflow', 'configuration',
'dynamicparam', 'exit'
]);
// PowerShell比较操作符
private readonly comparisonOperators = new Set([
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
'-like', '-notlike', '-match', '-notmatch',
'-contains', '-notcontains', '-in', '-notin',
'-is', '-isnot', '-as'
]);
// PowerShell逻辑操作符
private readonly logicalOperators = new Set([
'-and', '-or', '-not', '-xor', '-band', '-bor', '-bxor', '-bnot'
]);
constructor(code: string) {
this.code = code;
}
/**
* 对代码进行词法分析返回token数组
*/
public tokenize(): Token[] {
this.position = 0;
this.line = 1;
this.column = 1;
this.tokens = [];
while (this.position < this.code.length) {
this.skipWhitespace();
if (this.position >= this.code.length) {
break;
}
const token = this.nextToken();
if (token) {
this.tokens.push(token);
}
}
this.tokens.push({
type: TokenType.EOF,
value: '',
line: this.line,
column: this.column,
startIndex: this.position,
endIndex: this.position
});
return this.tokens;
}
private nextToken(): Token | null {
const startPos = this.position;
const startLine = this.line;
const startColumn = this.column;
const char = this.code[this.position];
// 处理换行
if (char === '\n') {
this.advance();
return this.createToken(TokenType.NEWLINE, '\n', startPos, startLine, startColumn);
}
// 处理注释
if (char === '#') {
return this.tokenizeComment(startPos, startLine, startColumn);
}
// 处理多行注释
if (char === '<' && this.peek() === '#') {
return this.tokenizeMultilineComment(startPos, startLine, startColumn);
}
// 处理字符串
if (char === '"' || char === "'") {
return this.tokenizeString(startPos, startLine, startColumn);
}
// 处理Here-String
if (char === '@' && (this.peek() === '"' || this.peek() === "'")) {
return this.tokenizeHereString(startPos, startLine, startColumn);
}
// 处理哈希表字面量 @{
if (char === '@' && this.peek() === '{') {
this.advance(); // skip '@'
this.advance(); // skip '{'
return this.createToken(TokenType.LEFT_BRACE, '@{', startPos, startLine, startColumn);
}
// 处理变量
if (char === '$') {
return this.tokenizeVariable(startPos, startLine, startColumn);
}
// 处理数字
if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek()))) {
return this.tokenizeNumber(startPos, startLine, startColumn);
}
// 处理操作符和分隔符
const operatorToken = this.tokenizeOperator(startPos, startLine, startColumn);
if (operatorToken) {
return operatorToken;
}
// 优先处理PowerShell比较操作符以-开头)
if (char === '-' && this.isIdentifierStart(this.peek())) {
const potentialOperator = this.peekPowerShellOperator();
if (potentialOperator) {
return this.tokenizePowerShellOperator(startPos, startLine, startColumn);
}
// 如果不是操作符,可能是参数
return this.tokenizeParameter(startPos, startLine, startColumn);
}
// 处理标识符包括cmdlet和关键字
if (this.isIdentifierStart(char)) {
return this.tokenizeIdentifier(startPos, startLine, startColumn);
}
// 处理PowerShell特殊字符
if (char === '?') {
this.advance();
return this.createToken(TokenType.OPERATOR, char, startPos, startLine, startColumn);
}
// 处理独立的减号(可能是负数或减法)
if (char === '-') {
this.advance();
return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn);
}
// 处理其他可能的特殊字符,作为标识符处理而不是未知字符
if (this.isPrintableChar(char)) {
this.advance();
return this.createToken(TokenType.IDENTIFIER, char, startPos, startLine, startColumn);
}
// 真正的未知字符(非打印字符等)
this.advance();
return this.createToken(TokenType.UNKNOWN, char, startPos, startLine, startColumn);
}
private tokenizeComment(startPos: number, startLine: number, startColumn: number): Token {
let value = '';
while (this.position < this.code.length && this.code[this.position] !== '\n') {
value += this.code[this.position];
this.advance();
}
return this.createToken(TokenType.COMMENT, value, startPos, startLine, startColumn);
}
private tokenizeMultilineComment(startPos: number, startLine: number, startColumn: number): Token {
let value = '';
this.advance(); // skip '<'
this.advance(); // skip '#'
value += '<#';
while (this.position < this.code.length - 1) {
if (this.code[this.position] === '#' && this.code[this.position + 1] === '>') {
value += '#>';
this.advance();
this.advance();
break;
}
value += this.code[this.position];
this.advance();
}
return this.createToken(TokenType.MULTILINE_COMMENT, value, startPos, startLine, startColumn);
}
private tokenizeString(startPos: number, startLine: number, startColumn: number): Token {
const quote = this.code[this.position];
let value = quote;
this.advance();
while (this.position < this.code.length) {
const char = this.code[this.position];
value += char;
if (char === quote) {
this.advance();
break;
}
// 处理转义字符
if (char === '`' && quote === '"') {
this.advance();
if (this.position < this.code.length) {
value += this.code[this.position];
this.advance();
}
} else {
this.advance();
}
}
return this.createToken(TokenType.STRING, value, startPos, startLine, startColumn);
}
private tokenizeHereString(startPos: number, startLine: number, startColumn: number): Token {
const quote = this.code[this.position + 1]; // " or '
let value = `@${quote}`;
this.advance(); // skip '@'
this.advance(); // skip quote
while (this.position < this.code.length - 1) {
if (this.code[this.position] === quote && this.code[this.position + 1] === '@') {
value += `${quote}@`;
this.advance();
this.advance();
break;
}
value += this.code[this.position];
this.advance();
}
return this.createToken(TokenType.HERE_STRING, value, startPos, startLine, startColumn);
}
private tokenizeVariable(startPos: number, startLine: number, startColumn: number): Token {
let value = '$';
this.advance(); // skip '$'
// 处理特殊变量如 $_, $$, $^
const specialVars = ['_', '$', '^', '?'];
if (specialVars.includes(this.code[this.position])) {
value += this.code[this.position];
this.advance();
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
}
// 处理大括号变量 ${variable name}
if (this.code[this.position] === '{') {
this.advance(); // skip '{'
value += '{';
while (this.position < this.code.length && this.code[this.position] !== '}') {
value += this.code[this.position];
this.advance();
}
if (this.position < this.code.length) {
value += '}';
this.advance(); // skip '}'
}
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
}
// 普通变量名
while (this.position < this.code.length && this.isIdentifierChar(this.code[this.position])) {
value += this.code[this.position];
this.advance();
}
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
}
private tokenizeNumber(startPos: number, startLine: number, startColumn: number): Token {
let value = '';
let hasDecimal = false;
while (this.position < this.code.length) {
const char = this.code[this.position];
if (this.isDigit(char)) {
value += char;
this.advance();
} else if (char === '.' && !hasDecimal && this.isDigit(this.peek())) {
hasDecimal = true;
value += char;
this.advance();
} else {
break;
}
}
// 检查是否有PowerShell数字单位后缀KB, MB, GB, TB, PB
const unitPattern = /^(KB|MB|GB|TB|PB)/i;
const remainingCode = this.code.substring(this.position);
const unitMatch = remainingCode.match(unitPattern);
if (unitMatch) {
value += unitMatch[0]; // 使用 [0] 获取完整匹配
// 移动position到单位后面
for (let i = 0; i < unitMatch[0].length; i++) {
this.advance();
}
}
return this.createToken(TokenType.NUMBER, value, startPos, startLine, startColumn);
}
private tokenizeOperator(startPos: number, startLine: number, startColumn: number): Token | null {
const char = this.code[this.position];
// 双字符操作符
const twoChar = this.code.substring(this.position, this.position + 2);
const doubleOperators = ['==', '!=', '<=', '>=', '++', '--', '+=', '-=', '*=', '/=', '%='];
if (doubleOperators.includes(twoChar)) {
this.advance();
this.advance();
return this.createToken(TokenType.OPERATOR, twoChar, startPos, startLine, startColumn);
}
// 单字符操作符
switch (char) {
case '=':
this.advance();
return this.createToken(TokenType.ASSIGNMENT, char, startPos, startLine, startColumn);
case '+':
case '*':
case '/':
case '%':
this.advance();
return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn);
case '-':
// 不在这里处理'-'让PowerShell操作符检查优先处理
return null;
case '(':
this.advance();
return this.createToken(TokenType.LEFT_PAREN, char, startPos, startLine, startColumn);
case ')':
this.advance();
return this.createToken(TokenType.RIGHT_PAREN, char, startPos, startLine, startColumn);
case '{':
this.advance();
return this.createToken(TokenType.LEFT_BRACE, char, startPos, startLine, startColumn);
case '}':
this.advance();
return this.createToken(TokenType.RIGHT_BRACE, char, startPos, startLine, startColumn);
case '[':
// 检查是否是PowerShell类型转换 [type]
const typePattern = this.peekTypeConversion();
if (typePattern) {
return this.tokenizeTypeConversion(startPos, startLine, startColumn);
}
this.advance();
return this.createToken(TokenType.LEFT_BRACKET, char, startPos, startLine, startColumn);
case ']':
this.advance();
return this.createToken(TokenType.RIGHT_BRACKET, char, startPos, startLine, startColumn);
case ';':
this.advance();
return this.createToken(TokenType.SEMICOLON, char, startPos, startLine, startColumn);
case ',':
this.advance();
return this.createToken(TokenType.COMMA, char, startPos, startLine, startColumn);
case '.':
this.advance();
return this.createToken(TokenType.DOT, char, startPos, startLine, startColumn);
case '|':
this.advance();
return this.createToken(TokenType.PIPE, char, startPos, startLine, startColumn);
}
return null;
}
private tokenizeIdentifier(startPos: number, startLine: number, startColumn: number): Token {
let value = '';
// 改进的标识符识别支持PowerShell cmdlet格式动词-名词)
while (this.position < this.code.length) {
const char = this.code[this.position];
if (this.isIdentifierChar(char)) {
value += char;
this.advance();
} else if (char === '-' && value.length > 0 && this.isIdentifierStart(this.peek())) {
// 检查是否是cmdlet格式动词-名词)
const nextPart = this.peekIdentifierPart();
if (nextPart && !this.isPowerShellOperator('-' + nextPart)) {
// 这是cmdlet名字的一部分继续
value += char;
this.advance();
} else {
// 这可能是操作符,停止
break;
}
} else {
break;
}
}
const lowerValue = value.toLowerCase();
// 检查是否是关键字
if (this.keywords.has(lowerValue)) {
return this.createToken(this.getKeywordTokenType(lowerValue), value, startPos, startLine, startColumn);
}
// 检查是否是函数(以动词-名词格式)
if (this.isCmdletName(value)) {
return this.createToken(TokenType.CMDLET, value, startPos, startLine, startColumn);
}
return this.createToken(TokenType.IDENTIFIER, value, startPos, startLine, startColumn);
}
private tokenizeParameter(startPos: number, startLine: number, startColumn: number): Token {
let value = '';
while (this.position < this.code.length && (this.isIdentifierChar(this.code[this.position]) || this.code[this.position] === '-')) {
value += this.code[this.position];
this.advance();
}
const lowerValue = value.toLowerCase();
// 检查是否是比较操作符
if (this.comparisonOperators.has(lowerValue)) {
return this.createToken(TokenType.COMPARISON, value, startPos, startLine, startColumn);
}
// 检查是否是逻辑操作符
if (this.logicalOperators.has(lowerValue)) {
return this.createToken(TokenType.LOGICAL, value, startPos, startLine, startColumn);
}
return this.createToken(TokenType.PARAMETER, value, startPos, startLine, startColumn);
}
private getKeywordTokenType(keyword: string): TokenType {
switch (keyword) {
case 'if': return TokenType.IF;
case 'else': return TokenType.ELSE;
case 'elseif': return TokenType.ELSEIF;
case 'while': return TokenType.WHILE;
case 'for': return TokenType.FOR;
case 'foreach': return TokenType.FOREACH;
case 'switch': return TokenType.SWITCH;
case 'try': return TokenType.TRY;
case 'catch': return TokenType.CATCH;
case 'finally': return TokenType.FINALLY;
case 'function': return TokenType.FUNCTION;
default: return TokenType.KEYWORD;
}
}
private isCmdletName(name: string): boolean {
// PowerShell cmdlet通常遵循 Verb-Noun 格式,可能包含多个连字符
const verbNounPattern = /^[A-Za-z]+(-[A-Za-z]+)+$/;
return verbNounPattern.test(name);
}
private peekPowerShellOperator(): string | null {
// 检查是否是PowerShell比较或逻辑操作符
const operatorPatterns = [
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
'-like', '-notlike', '-match', '-notmatch',
'-contains', '-notcontains', '-in', '-notin',
'-is', '-isnot', '-as',
'-and', '-or', '-not', '-xor',
'-band', '-bor', '-bxor', '-bnot'
];
for (const op of operatorPatterns) {
if (this.matchesOperator(op)) {
return op;
}
}
return null;
}
private matchesOperator(operator: string): boolean {
if (this.position + operator.length > this.code.length) {
return false;
}
const substr = this.code.substring(this.position, this.position + operator.length);
if (substr.toLowerCase() !== operator.toLowerCase()) {
return false;
}
// 确保操作符后面不是字母数字字符(避免匹配部分单词)
const nextChar = this.position + operator.length < this.code.length
? this.code[this.position + operator.length]
: ' ';
return !this.isIdentifierChar(nextChar);
}
private tokenizePowerShellOperator(startPos: number, startLine: number, startColumn: number): Token {
const operator = this.peekPowerShellOperator();
if (!operator) {
// 如果不是操作符,作为参数处理
return this.tokenizeParameter(startPos, startLine, startColumn);
}
// 消费操作符字符
for (let i = 0; i < operator.length; i++) {
this.advance();
}
const lowerOp = operator.toLowerCase();
// 确定操作符类型
if (this.comparisonOperators.has(lowerOp)) {
return this.createToken(TokenType.COMPARISON, operator, startPos, startLine, startColumn);
} else if (this.logicalOperators.has(lowerOp)) {
return this.createToken(TokenType.LOGICAL, operator, startPos, startLine, startColumn);
} else {
return this.createToken(TokenType.OPERATOR, operator, startPos, startLine, startColumn);
}
}
private peekIdentifierPart(): string | null {
if (this.position + 1 >= this.code.length) {
return null;
}
let result = '';
let pos = this.position + 1; // 跳过连字符
while (pos < this.code.length && this.isIdentifierChar(this.code[pos])) {
result += this.code[pos];
pos++;
}
return result.length > 0 ? result : null;
}
private isPowerShellOperator(text: string): boolean {
const lowerText = text.toLowerCase();
return this.comparisonOperators.has(lowerText) || this.logicalOperators.has(lowerText);
}
private peekTypeConversion(): string | null {
// 检查是否是PowerShell类型转换如 [int], [string], [datetime] 等
if (this.code[this.position] !== '[') {
return null;
}
let pos = this.position + 1; // 跳过 '['
let typeContent = '';
// 查找类型名称
while (pos < this.code.length && this.code[pos] !== ']') {
typeContent += this.code[pos];
pos++;
}
if (pos >= this.code.length || this.code[pos] !== ']') {
return null; // 没有找到匹配的 ']'
}
// 检查是否是有效的PowerShell类型
const validTypes = [
'int', 'int32', 'int64', 'string', 'bool', 'boolean', 'char', 'byte',
'double', 'float', 'decimal', 'long', 'short', 'datetime', 'timespan',
'array', 'hashtable', 'object', 'psobject', 'xml', 'scriptblock',
'guid', 'uri', 'version', 'regex', 'mailaddress', 'ipaddress'
];
const lowerType = typeContent.toLowerCase().trim();
if (validTypes.includes(lowerType) || lowerType.includes('.')) {
return `[${typeContent}]`;
}
return null;
}
private tokenizeTypeConversion(startPos: number, startLine: number, startColumn: number): Token {
const typeConversion = this.peekTypeConversion();
if (!typeConversion) {
// 这不应该发生,但作为安全措施
this.advance();
return this.createToken(TokenType.LEFT_BRACKET, '[', startPos, startLine, startColumn);
}
// 消费整个类型转换
for (let i = 0; i < typeConversion.length; i++) {
this.advance();
}
return this.createToken(TokenType.IDENTIFIER, typeConversion, startPos, startLine, startColumn);
}
private isIdentifierStart(char: string): boolean {
return /[a-zA-Z_]/.test(char);
}
private isIdentifierChar(char: string): boolean {
return /[a-zA-Z0-9_]/.test(char);
}
private isDigit(char: string): boolean {
return char >= '0' && char <= '9';
}
private isPrintableChar(char: string): boolean {
// 检查是否为可打印字符(非控制字符)
const charCode = char.charCodeAt(0);
return charCode >= 32 && charCode <= 126;
}
private advance(): void {
if (this.position < this.code.length) {
if (this.code[this.position] === '\n') {
this.line++;
this.column = 1;
} else {
this.column++;
}
this.position++;
}
}
private peek(): string {
return this.position + 1 < this.code.length ? this.code[this.position + 1] : '';
}
private skipWhitespace(): void {
while (this.position < this.code.length) {
const char = this.code[this.position];
if (char === ' ' || char === '\t' || char === '\r') {
this.advance();
} else {
break;
}
}
}
private createToken(type: TokenType, value: string, startPos: number, line: number, column: number): Token {
return {
type,
value,
line,
column,
startIndex: startPos,
endIndex: this.position
};
}
}

View File

@@ -0,0 +1,821 @@
/**
* PowerShell 语法分析器 (Parser)
* 将词法分析器产生的tokens转换为抽象语法树(AST)
*/
import { Token, TokenType } from './lexer';
import {
ASTNode,
ScriptBlockAst,
StatementAst,
ExpressionAst,
PipelineAst,
CommandAst,
AssignmentAst,
VariableAst,
LiteralAst,
BinaryExpressionAst,
IfStatementAst,
FunctionDefinitionAst,
ParameterAst,
ASTNodeFactory,
CommentAst,
PipelineElementAst,
ElseIfClauseAst,
UnaryExpressionAst,
ParenthesizedExpressionAst
} from './ast';
export class PowerShellParser {
private tokens: Token[];
private currentIndex: number = 0;
private comments: CommentAst[] = [];
private originalCode: string;
constructor(tokens: Token[], originalCode: string = '') {
this.tokens = tokens;
this.currentIndex = 0;
this.originalCode = originalCode;
}
/**
* 解析tokens生成AST
*/
public parse(): ScriptBlockAst {
const statements: StatementAst[] = [];
while (!this.isAtEnd()) {
// 跳过空白和换行
this.skipWhitespaceAndNewlines();
if (this.isAtEnd()) {
break;
}
// 处理注释
if (this.match(TokenType.COMMENT, TokenType.MULTILINE_COMMENT)) {
const comment = this.parseComment();
this.comments.push(comment);
continue;
}
const statement = this.parseStatement();
if (statement) {
statements.push(statement);
}
}
const start = this.tokens.length > 0 ? this.tokens[0].startIndex : 0;
const end = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1].endIndex : 0;
const line = this.tokens.length > 0 ? this.tokens[0].line : 1;
const column = this.tokens.length > 0 ? this.tokens[0].column : 1;
return ASTNodeFactory.createScriptBlock(statements, start, end, line, column);
}
public getComments(): CommentAst[] {
return this.comments;
}
private parseStatement(): StatementAst | null {
// 函数定义
if (this.check(TokenType.FUNCTION)) {
return this.parseFunctionDefinition();
}
// 控制流语句
if (this.check(TokenType.IF)) {
return this.parseIfStatement();
}
// 赋值或管道
return this.parsePipeline();
}
private parseFunctionDefinition(): FunctionDefinitionAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
this.consume(TokenType.FUNCTION, "Expected 'function'");
// 函数名可能是CMDLET类型如Get-Something或IDENTIFIER
let nameToken: Token;
if (this.check(TokenType.CMDLET)) {
nameToken = this.consume(TokenType.CMDLET, "Expected function name");
} else {
nameToken = this.consume(TokenType.IDENTIFIER, "Expected function name");
}
const name = nameToken.value;
// 解析参数
const parameters: ParameterAst[] = [];
if (this.match(TokenType.LEFT_PAREN)) {
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
const param = this.parseParameter();
if (param) {
parameters.push(param);
}
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
}
// 解析函数体
const body = this.parseScriptBlock();
const end = this.previous().endIndex;
return ASTNodeFactory.createFunctionDefinition(name, parameters, body, start, end, line, column);
}
private parseIfStatement(): IfStatementAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
this.consume(TokenType.IF, "Expected 'if'");
// PowerShell的if语句可能有括号也可能没有
const hasParens = this.check(TokenType.LEFT_PAREN);
if (hasParens) {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'if'");
}
const condition = this.parseExpression();
if (hasParens) {
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after if condition");
}
const ifBody = this.parseScriptBlock();
const elseIfClauses: ElseIfClauseAst[] = [];
let elseBody: ScriptBlockAst | undefined;
// 处理 elseif 子句
while (this.match(TokenType.ELSEIF)) {
const elseIfStart = this.previous().startIndex;
const elseIfLine = this.previous().line;
const elseIfColumn = this.previous().column;
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'elseif'");
const elseIfCondition = this.parseExpression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after elseif condition");
const elseIfBody = this.parseScriptBlock();
const elseIfEnd = this.previous().endIndex;
elseIfClauses.push({
type: 'ElseIfClause',
condition: elseIfCondition,
body: elseIfBody,
start: elseIfStart,
end: elseIfEnd,
line: elseIfLine,
column: elseIfColumn
});
}
// 处理 else 子句
if (this.match(TokenType.ELSE)) {
elseBody = this.parseScriptBlock();
}
const end = this.previous().endIndex;
return ASTNodeFactory.createIfStatement(condition, ifBody, elseIfClauses, elseBody, start, end, line, column);
}
private parsePipeline(): PipelineAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
const elements: PipelineElementAst[] = [];
// 解析第一个元素
const firstElement = this.parsePipelineElement();
elements.push(firstElement);
// 解析管道链
while (this.match(TokenType.PIPE)) {
const element = this.parsePipelineElement();
elements.push(element);
}
const end = this.previous().endIndex;
return ASTNodeFactory.createPipeline(elements, start, end, line, column);
}
private parsePipelineElement(): PipelineElementAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
const expression = this.parseAssignment();
const end = this.previous().endIndex;
return {
type: 'PipelineElement',
expression,
start,
end,
line,
column
};
}
private parseAssignment(): ExpressionAst {
const expr = this.parseLogicalOr();
if (this.match(TokenType.ASSIGNMENT)) {
const operator = this.previous().value;
const right = this.parseAssignment();
return ASTNodeFactory.createAssignment(
expr,
operator,
right,
expr.start,
right.end,
expr.line,
expr.column
);
}
return expr;
}
private parseLogicalOr(): ExpressionAst {
let expr = this.parseLogicalAnd();
while (this.match(TokenType.LOGICAL)) {
const operator = this.previous().value.toLowerCase();
if (operator === '-or' || operator === '-xor') {
const right = this.parseLogicalAnd();
expr = ASTNodeFactory.createBinaryExpression(
expr,
this.previous().value, // 使用原始大小写
right,
expr.start,
right.end,
expr.line,
expr.column
);
} else {
// 如果不是预期的操作符,回退
this.currentIndex--;
break;
}
}
return expr;
}
private parseLogicalAnd(): ExpressionAst {
let expr = this.parseComparison();
while (this.match(TokenType.LOGICAL)) {
const operator = this.previous().value.toLowerCase();
if (operator === '-and') {
const right = this.parseComparison();
expr = ASTNodeFactory.createBinaryExpression(
expr,
this.previous().value, // 使用原始大小写
right,
expr.start,
right.end,
expr.line,
expr.column
);
} else {
// 如果不是预期的操作符,回退
this.currentIndex--;
break;
}
}
return expr;
}
private parseComparison(): ExpressionAst {
let expr = this.parseArithmetic();
while (this.match(TokenType.COMPARISON)) {
const operator = this.previous().value;
const right = this.parseArithmetic();
expr = ASTNodeFactory.createBinaryExpression(
expr,
operator,
right,
expr.start,
right.end,
expr.line,
expr.column
);
}
return expr;
}
private parseArithmetic(): ExpressionAst {
let expr = this.parseMultiplicative();
while (this.match(TokenType.ARITHMETIC)) {
const token = this.previous();
if (token.value === '+' || token.value === '-') {
const operator = token.value;
const right = this.parseMultiplicative();
expr = ASTNodeFactory.createBinaryExpression(
expr,
operator,
right,
expr.start,
right.end,
expr.line,
expr.column
);
}
}
return expr;
}
private parseMultiplicative(): ExpressionAst {
let expr = this.parseUnary();
while (this.match(TokenType.ARITHMETIC)) {
const token = this.previous();
if (token.value === '*' || token.value === '/' || token.value === '%') {
const operator = token.value;
const right = this.parseUnary();
expr = ASTNodeFactory.createBinaryExpression(
expr,
operator,
right,
expr.start,
right.end,
expr.line,
expr.column
);
}
}
return expr;
}
private parseUnary(): ExpressionAst {
if (this.match(TokenType.LOGICAL)) {
const token = this.previous();
const operator = token.value.toLowerCase();
if (operator === '-not') {
const operand = this.parseUnary();
return {
type: 'UnaryExpression',
operator: token.value, // 使用原始大小写
operand,
start: token.startIndex,
end: operand.end,
line: token.line,
column: token.column
} as UnaryExpressionAst;
} else {
// 如果不是-not回退token
this.currentIndex--;
}
}
// 处理算术一元操作符(+, -
if (this.match(TokenType.ARITHMETIC)) {
const token = this.previous();
if (token.value === '+' || token.value === '-') {
const operand = this.parseUnary();
return {
type: 'UnaryExpression',
operator: token.value,
operand,
start: token.startIndex,
end: operand.end,
line: token.line,
column: token.column
} as UnaryExpressionAst;
} else {
// 如果不是一元操作符,回退
this.currentIndex--;
}
}
return this.parsePrimary();
}
private parsePrimary(): ExpressionAst {
// 变量
if (this.match(TokenType.VARIABLE)) {
const token = this.previous();
return ASTNodeFactory.createVariable(
token.value,
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 字符串字面量
if (this.match(TokenType.STRING, TokenType.HERE_STRING)) {
const token = this.previous();
return ASTNodeFactory.createLiteral(
token.value,
'String',
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 数字字面量
if (this.match(TokenType.NUMBER)) {
const token = this.previous();
const value = parseFloat(token.value);
return ASTNodeFactory.createLiteral(
value,
'Number',
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 命令调用 - 扩展支持更多token类型
if (this.match(TokenType.CMDLET, TokenType.IDENTIFIER)) {
return this.parseCommand();
}
// 处理看起来像cmdlet但被错误标记的标识符
if (this.check(TokenType.IDENTIFIER) && this.current().value.includes('-')) {
this.advance();
return this.parseCommand();
}
// 哈希表 @{...}
if (this.check(TokenType.LEFT_BRACE) && this.current().value === '@{') {
return this.parseHashtable();
}
// 脚本块表达式 {...} - 已在parseHashtableValue中处理
// 这里不需要处理,因为独立的脚本块很少见
// 括号表达式
if (this.match(TokenType.LEFT_PAREN)) {
const expr = this.parseExpression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression");
return {
type: 'ParenthesizedExpression',
expression: expr,
start: this.previous().startIndex,
end: this.previous().endIndex,
line: this.previous().line,
column: this.previous().column
} as ParenthesizedExpressionAst;
}
// 对于不认识的token作为普通标识符处理而不是抛出异常
const token = this.advance();
return ASTNodeFactory.createLiteral(
token.value,
'String', // 将未识别的token作为字符串处理
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
private parseCommand(): CommandAst {
const start = this.previous().startIndex;
const line = this.previous().line;
const column = this.previous().column;
const commandName = this.previous().value;
const parameters: ParameterAst[] = [];
const args: ExpressionAst[] = [];
// 解析参数和参数值
while (!this.isAtEnd() &&
!this.check(TokenType.PIPE) &&
!this.check(TokenType.NEWLINE) &&
!this.check(TokenType.SEMICOLON) &&
!this.check(TokenType.RIGHT_PAREN) &&
!this.check(TokenType.RIGHT_BRACE)) {
if (this.match(TokenType.PARAMETER)) {
const paramToken = this.previous();
const param: ParameterAst = {
type: 'Parameter',
name: paramToken.value,
start: paramToken.startIndex,
end: paramToken.endIndex,
line: paramToken.line,
column: paramToken.column
};
// 检查参数是否有值
if (!this.check(TokenType.PARAMETER) &&
!this.check(TokenType.PIPE) &&
!this.check(TokenType.NEWLINE) &&
!this.check(TokenType.SEMICOLON)) {
param.value = this.parsePrimary();
}
parameters.push(param);
} else {
// 位置参数
const arg = this.parsePrimary();
args.push(arg);
}
}
const end = this.previous().endIndex;
return ASTNodeFactory.createCommand(commandName, parameters, args, start, end, line, column);
}
private parseParameter(): ParameterAst | null {
if (this.match(TokenType.PARAMETER)) {
const token = this.previous();
const param: ParameterAst = {
type: 'Parameter',
name: token.value,
start: token.startIndex,
end: token.endIndex,
line: token.line,
column: token.column
};
// 检查是否有参数值
if (this.match(TokenType.ASSIGNMENT)) {
param.value = this.parseExpression();
}
return param;
}
return null;
}
private parseScriptBlock(): ScriptBlockAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
this.consume(TokenType.LEFT_BRACE, "Expected '{'");
const statements: StatementAst[] = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
this.skipWhitespaceAndNewlines();
if (this.check(TokenType.RIGHT_BRACE)) {
break;
}
const statement = this.parseStatement();
if (statement) {
statements.push(statement);
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}'");
const end = this.previous().endIndex;
return ASTNodeFactory.createScriptBlock(statements, start, end, line, column);
}
private parseExpression(): ExpressionAst {
return this.parseAssignment();
}
private parseComment(): CommentAst {
const token = this.previous();
const isMultiline = token.type === TokenType.MULTILINE_COMMENT;
return ASTNodeFactory.createComment(
token.value,
isMultiline,
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 辅助方法
private match(...types: TokenType[]): boolean {
for (const type of types) {
if (this.check(type)) {
this.advance();
return true;
}
}
return false;
}
private check(type: TokenType): boolean {
if (this.isAtEnd()) return false;
return this.current().type === type;
}
private advance(): Token {
if (!this.isAtEnd()) this.currentIndex++;
return this.previous();
}
private isAtEnd(): boolean {
return this.currentIndex >= this.tokens.length || this.current().type === TokenType.EOF;
}
private current(): Token {
if (this.currentIndex >= this.tokens.length) {
return this.tokens[this.tokens.length - 1];
}
return this.tokens[this.currentIndex];
}
private previous(): Token {
return this.tokens[this.currentIndex - 1];
}
private consume(type: TokenType, message: string): Token {
if (this.check(type)) return this.advance();
const current = this.current();
throw new Error(`${message}. Got ${current.type}(${current.value}) at line ${current.line}, column ${current.column}`);
}
private parseHashtable(): ExpressionAst {
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
// 消费 @{
this.advance();
const entries: any[] = [];
// 解析哈希表内容
if (!this.check(TokenType.RIGHT_BRACE)) {
do {
// 解析键 - 只接受简单的标识符或字符串
const key = this.parseHashtableKey();
// 消费 =
this.consume(TokenType.ASSIGNMENT, "Expected '=' after hashtable key");
// 解析值
const value = this.parseHashtableValue();
entries.push({
type: 'HashtableEntry',
key,
value,
start: key.start,
end: value.end,
line: key.line,
column: key.column
});
} while (this.match(TokenType.SEMICOLON));
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after hashtable entries");
const end = this.previous().endIndex;
return {
type: 'Hashtable',
entries,
start,
end,
line,
column
} as any;
}
private parseHashtableKey(): ExpressionAst {
// 哈希表键只能是简单的标识符或字符串
if (this.match(TokenType.STRING, TokenType.HERE_STRING)) {
const token = this.previous();
return ASTNodeFactory.createLiteral(
token.value,
'String',
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 接受各种可能的标识符类型作为哈希表键
if (this.match(TokenType.IDENTIFIER, TokenType.CMDLET, TokenType.KEYWORD)) {
const token = this.previous();
return ASTNodeFactory.createLiteral(
token.value,
'String',
token.startIndex,
token.endIndex,
token.line,
token.column
);
}
// 对于任何其他类型的token尝试作为字面量处理
const currentToken = this.current();
this.advance();
return ASTNodeFactory.createLiteral(
currentToken.value,
'String',
currentToken.startIndex,
currentToken.endIndex,
currentToken.line,
currentToken.column
);
}
private parseHashtableValue(): ExpressionAst {
// 哈希表值可以是任何表达式
if (this.check(TokenType.LEFT_BRACE)) {
// 这是一个脚本块 {expression} - 完全绕过复杂解析
const start = this.current().startIndex;
const line = this.current().line;
const column = this.current().column;
// 直接从原始代码中提取脚本块内容
const startPos = this.current().startIndex;
this.advance(); // 消费 {
let braceLevel = 1;
let endPos = this.current().startIndex;
// 找到匹配的右大括号位置
while (!this.isAtEnd() && braceLevel > 0) {
const token = this.current();
if (token.type === TokenType.LEFT_BRACE) {
braceLevel++;
} else if (token.type === TokenType.RIGHT_BRACE) {
braceLevel--;
if (braceLevel === 0) {
endPos = token.startIndex;
break;
}
}
this.advance();
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after script block");
const end = this.previous().endIndex;
// 从原始代码中提取内容(从 { 后到 } 前)
const rawContent = this.getOriginalCodeSlice(startPos + 1, endPos);
return {
type: 'ScriptBlockExpression',
rawContent: rawContent.trim(), // 去掉首尾空白
start,
end,
line,
column
} as any;
}
// 对于其他值,使用简单的解析
return this.parsePrimary();
}
private getOriginalCodeSlice(start: number, end: number): string {
// 直接从原始代码中提取片段
if (this.originalCode) {
return this.originalCode.substring(start, end);
}
// 回退到基于token重建如果没有原始代码
let result = '';
for (const token of this.tokens) {
if (token.startIndex >= start && token.endIndex <= end) {
result += token.value;
}
}
return result;
}
private skipWhitespaceAndNewlines(): void {
while (this.match(TokenType.WHITESPACE, TokenType.NEWLINE)) {
// 继续跳过
}
}
}

View File

@@ -0,0 +1,109 @@
import { createScalaPrinter } from "./printer";
import { parse, type ScalaCstNode, type IToken } from "./scala-parser";
import { type Plugin, type SupportOption } from "prettier";
/**
* Prettierがサポートする言語の定義
*/
const languages = [
{
name: "Scala",
parsers: ["scala"],
extensions: [".scala", ".sc"],
vscodeLanguageIds: ["scala"],
},
];
/**
* Scalaパーサーの定義
*/
const parsers = {
scala: {
parse: (text: string) => {
const result = parse(text);
// シンプルなコメント保持: ASTに格納してvisitorで処理
const ast = {
...result.cst,
comments: [], // Prettierの検証を回避
originalComments: result.comments || [], // プラグイン独自のコメント格納
type: "compilationUnit",
};
return ast;
},
astFormat: "scala-cst",
locStart: (node: ScalaCstNode | IToken) => {
// Handle comment tokens (from Chevrotain lexer)
if ("startOffset" in node && node.startOffset !== undefined) {
return node.startOffset;
}
// Handle CST nodes
if ("location" in node && node.location?.startOffset !== undefined) {
return node.location.startOffset;
}
return 0;
},
locEnd: (node: ScalaCstNode | IToken) => {
// Handle comment tokens (from Chevrotain lexer)
if ("endOffset" in node && node.endOffset !== undefined) {
return node.endOffset + 1; // Chevrotain endOffset is inclusive, Prettier expects exclusive
}
// Handle CST nodes
if ("location" in node && node.location?.endOffset !== undefined) {
return node.location.endOffset + 1; // Chevrotain endOffset is inclusive, Prettier expects exclusive
}
return 1;
},
hasPragma: () => false,
},
};
/**
* プリンターの定義
*/
const printers = {
"scala-cst": createScalaPrinter(),
};
/**
* プラグインオプションscalafmt互換性 - フェーズ1
*/
const options: Record<string, SupportOption> = {
// Prettier standard options with Scala-specific defaults
semi: {
type: "boolean",
default: false, // Scala convention: omit semicolons
description: "Add semicolons at the end of statements",
category: "Global",
} as const,
// Deprecated options (backward compatibility)
scalaLineWidth: {
type: "int",
default: 80,
description: "Maximum line width (DEPRECATED: use printWidth instead)",
category: "Scala",
} as const,
scalaIndentStyle: {
type: "choice",
default: "spaces",
choices: [
{ value: "spaces", description: "Use spaces for indentation" },
{ value: "tabs", description: "Use tabs for indentation" },
],
description: "Indentation style (DEPRECATED: use useTabs instead)",
category: "Scala",
} as const,
};
/**
* Prettierプラグインのエクスポート
*/
const plugin: Plugin = {
languages,
parsers,
printers,
options,
};
export default plugin;

View File

@@ -0,0 +1,91 @@
import { CstNodeVisitor, type CSTNode } from "./visitor";
import type { ScalaCstNode, IToken } from "./scala-parser";
import { type Doc, type Printer, type AstPath, type Options } from "prettier";
/**
* Scala用のPrettierプリンターを作成
* @returns Prettierプリンターオブジェクト
*/
export function createScalaPrinter(): Printer {
return {
/**
* ASTードをフォーマット済みのテキストに変換
* @param path - 現在のノードへのパス
* @param options - Prettierオプション
* @param print - 子ノードを印刷するための関数
* @returns フォーマット済みのDoc
*/
print(
path: AstPath<ScalaCstNode>,
options: Options,
print: (path: AstPath) => Doc,
): Doc {
const node = path.getValue();
const visitor = new CstNodeVisitor();
const result = visitor.visit(node, {
path,
options: {
printWidth: options.printWidth,
tabWidth: options.tabWidth,
useTabs: options.useTabs,
semi: options.semi,
singleQuote: options.singleQuote,
trailingComma:
options.trailingComma === "es5" ? "all" : options.trailingComma,
},
print: (childNode: CSTNode) => {
// 子ノード用のモックパスを作成
const mockPath = {
getValue: () => childNode,
call: (fn: () => unknown) => fn(),
};
return String(print(mockPath as AstPath<unknown>));
},
indentLevel: 0,
});
// 文字列結果をPrettierのDocに変換
return result;
},
/**
* コメントを印刷
* @param path - コメントトークンへのパス
* @returns フォーマット済みのコメント
*/
printComment(path: AstPath<IToken>): Doc {
const comment = path.getValue();
if (!comment) return "";
// Chevrotainのimageプロパティを使用
if (typeof comment.image === "string") {
return comment.image;
}
// fallback
if (typeof comment.image === "string") {
return comment.image;
}
// デバッグ: コメント構造を確認
console.log("Unexpected comment structure in printComment:", comment);
return "";
},
canAttachComment(): boolean {
// コメント機能を一時的に無効化
return false;
},
willPrintOwnComments(): boolean {
return false; // Prettier標準のコメント処理を使用しない
},
insertPragma(text: string): string {
return text;
},
hasPrettierIgnore(): boolean {
return false;
},
isBlockComment(comment: IToken): boolean {
return comment.tokenType?.name === "BlockComment";
},
};
}

View File

@@ -0,0 +1,206 @@
import { ScalaLexer } from "./lexer";
import { parserInstance } from "./parser";
import type {
ParseResult,
ScalaCstNode,
TokenBounds,
LineColumn,
} from "./types";
import type { IToken, CstElement } from "chevrotain";
export { ScalaLexer, allTokens } from "./lexer";
export { ScalaParser, parserInstance } from "./parser";
export type {
ParseResult,
ScalaCstNode,
TokenBounds,
LineColumn,
} from "./types";
export type { IToken } from "chevrotain";
/**
* CSTードに位置情報を自動設定するヘルパー関数
* @param cst - 処理対象のCSTード
* @param tokens - 解析で使用されたトークンの配列
* @param text - 元のソースコードテキスト
* @returns 位置情報が付与されたCSTード
*/
function addLocationToCST(
cst: ScalaCstNode,
tokens: IToken[],
text: string,
): ScalaCstNode {
if (!cst || !tokens) return cst;
// テキストから行の開始位置を計算
const lineStarts = [0]; // 最初の行は0から始まる
for (let i = 0; i < text.length; i++) {
if (text[i] === "\n") {
lineStarts.push(i + 1);
}
}
// オフセットから行番号と列番号を取得
function getLineAndColumn(offset: number): LineColumn {
let line = 1;
for (let i = 0; i < lineStarts.length - 1; i++) {
if (offset >= lineStarts[i] && offset < lineStarts[i + 1]) {
line = i + 1;
break;
}
}
if (offset >= lineStarts[lineStarts.length - 1]) {
line = lineStarts.length;
}
const column = offset - lineStarts[line - 1] + 1;
return { line, column };
}
// トークンから最小・最大位置を計算
function findTokenBounds(node: ScalaCstNode): TokenBounds | null {
if (!node) return null;
let minStart = Infinity;
let maxEnd = -1;
function findTokensInNode(n: ScalaCstNode | IToken): void {
if (!n) return;
// トークンの場合
if (
"startOffset" in n &&
"endOffset" in n &&
n.startOffset !== undefined &&
n.endOffset !== undefined
) {
minStart = Math.min(minStart, n.startOffset);
maxEnd = Math.max(maxEnd, n.endOffset);
return;
}
// CSTードの場合
if ("children" in n && n.children) {
for (const children of Object.values(n.children)) {
if (Array.isArray(children)) {
children.forEach((child) => {
// CstElementをScalaCstNode | ITokenに安全に変換
if ("children" in child) {
findTokensInNode(child as ScalaCstNode);
} else {
findTokensInNode(child as IToken);
}
});
}
}
}
}
findTokensInNode(node);
if (minStart === Infinity || maxEnd === -1) {
return null;
}
return { start: minStart, end: maxEnd };
}
// 再帰的にCSTードに位置情報を設定
function setCSTLocation(node: ScalaCstNode): ScalaCstNode {
if (!node) return node;
// トークンの場合はそのまま返す
if (node.startOffset !== undefined) {
return node;
}
// CSTードの場合
if (node.children) {
// 子ノードを先に処理
const processedChildren: Record<string, CstElement[]> = {};
for (const [key, children] of Object.entries(node.children)) {
if (Array.isArray(children)) {
processedChildren[key] = children.map((child) => {
if ("children" in child) {
return setCSTLocation(child as ScalaCstNode);
}
return child; // IToken
});
}
}
// このノードの位置を計算
const bounds = findTokenBounds({ ...node, children: processedChildren });
if (bounds) {
const startLoc = getLineAndColumn(bounds.start);
const endLoc = getLineAndColumn(bounds.end);
return {
...node,
children: processedChildren,
location: {
startOffset: bounds.start,
endOffset: bounds.end,
startLine: startLoc.line,
endLine: endLoc.line,
startColumn: startLoc.column,
endColumn: endLoc.column,
},
};
} else {
return {
...node,
children: processedChildren,
};
}
}
return node;
}
return setCSTLocation(cst);
}
export function parse(text: string): ParseResult {
// Use legacy parser for now until modular parser is fixed
return parseLegacy(text);
}
// Legacy parser function (has left recursion issues)
export function parseLegacy(text: string): ParseResult {
// Tokenize
const lexResult = ScalaLexer.tokenize(text);
if (lexResult.errors.length > 0) {
throw new Error(
`Lexing errors: ${lexResult.errors.map((e) => e.message).join(", ")}`,
);
}
// Parse
parserInstance.input = lexResult.tokens;
const cst = parserInstance.compilationUnit();
if (parserInstance.errors.length > 0) {
throw new Error(
`Parsing errors: ${parserInstance.errors.map((e) => e.message).join(", ")}`,
);
}
// CSTに位置情報を追加
const cstWithLocation = addLocationToCST(
cst as ScalaCstNode,
lexResult.tokens,
text,
);
return {
cst: cstWithLocation,
errors: [],
comments: lexResult.groups.comments || [],
};
}
// Note: parseModular function was removed as the modular parser integration
// is still in development. Use the main parse() function instead.

View File

@@ -0,0 +1,479 @@
import { createToken, Lexer, ILexingResult } from "chevrotain";
// Keywords
export const Val = createToken({ name: "Val", pattern: /val\b/ });
export const Var = createToken({ name: "Var", pattern: /var\b/ });
export const Def = createToken({ name: "Def", pattern: /def\b/ });
export const Class = createToken({ name: "Class", pattern: /class\b/ });
export const ObjectKeyword = createToken({
name: "Object",
pattern: /object\b/,
});
export const Trait = createToken({ name: "Trait", pattern: /trait\b/ });
export const Extends = createToken({ name: "Extends", pattern: /extends\b/ });
export const With = createToken({ name: "With", pattern: /with\b/ });
export const If = createToken({ name: "If", pattern: /if\b/ });
export const Else = createToken({ name: "Else", pattern: /else\b/ });
export const While = createToken({ name: "While", pattern: /while\b/ });
export const For = createToken({ name: "For", pattern: /for\b/ });
export const Yield = createToken({ name: "Yield", pattern: /yield\b/ });
export const Return = createToken({ name: "Return", pattern: /return\b/ });
export const New = createToken({ name: "New", pattern: /new\b/ });
export const This = createToken({ name: "This", pattern: /this\b/ });
export const Super = createToken({ name: "Super", pattern: /super\b/ });
export const Package = createToken({ name: "Package", pattern: /package\b/ });
export const Import = createToken({ name: "Import", pattern: /import\b/ });
export const Case = createToken({ name: "Case", pattern: /case\b/ });
export const Match = createToken({ name: "Match", pattern: /match\b/ });
export const Try = createToken({ name: "Try", pattern: /try\b/ });
export const Catch = createToken({ name: "Catch", pattern: /catch\b/ });
export const Finally = createToken({ name: "Finally", pattern: /finally\b/ });
export const Throw = createToken({ name: "Throw", pattern: /throw\b/ });
export const Null = createToken({ name: "Null", pattern: /null\b/ });
export const True = createToken({ name: "True", pattern: /true\b/ });
export const False = createToken({ name: "False", pattern: /false\b/ });
export const NotImplemented = createToken({
name: "NotImplemented",
pattern: /\?\?\?/,
});
export const Type = createToken({ name: "Type", pattern: /type\b/ });
export const Private = createToken({ name: "Private", pattern: /private\b/ });
export const Protected = createToken({
name: "Protected",
pattern: /protected\b/,
});
export const Public = createToken({ name: "Public", pattern: /public\b/ });
export const Abstract = createToken({
name: "Abstract",
pattern: /abstract\b/,
});
export const Final = createToken({ name: "Final", pattern: /final\b/ });
export const Sealed = createToken({ name: "Sealed", pattern: /sealed\b/ });
export const Implicit = createToken({
name: "Implicit",
pattern: /implicit\b/,
});
export const Lazy = createToken({ name: "Lazy", pattern: /lazy\b/ });
export const Override = createToken({
name: "Override",
pattern: /override\b/,
});
export const Given = createToken({ name: "Given", pattern: /given\b/ });
export const Using = createToken({ name: "Using", pattern: /using\b/ });
export const To = createToken({ name: "To", pattern: /to\b/ });
export const Enum = createToken({ name: "Enum", pattern: /enum\b/ });
export const Array = createToken({ name: "Array", pattern: /Array\b/ });
export const Extension = createToken({
name: "Extension",
pattern: /extension\b/,
});
export const Export = createToken({ name: "Export", pattern: /export\b/ });
export const Opaque = createToken({ name: "Opaque", pattern: /opaque\b/ });
export const Inline = createToken({ name: "Inline", pattern: /inline\b/ });
export const Transparent = createToken({
name: "Transparent",
pattern: /transparent\b/,
});
// Identifiers (must come after keywords)
// Enhanced Unicode identifier support following Scala Language Specification
// Operator identifier for custom operators (e.g., +++, <~>, etc.)
export const OperatorIdentifier = createToken({
name: "OperatorIdentifier",
pattern: /[+\-*/%:&|^<>=!~?#@$\\]+/,
});
// Backward compatible with existing implementation, enhanced mathematical symbol support
// Supports: Latin, Greek, Cyrillic, CJK, Arabic, Hebrew, Mathematical symbols, Emojis (via surrogate pairs)
export const Identifier = createToken({
name: "Identifier",
pattern:
/(?:_[a-zA-Z0-9_$\u00C0-\u00FF\u0370-\u03FF\u0400-\u04FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0590-\u05FF\u0600-\u06FF\u2200-\u22FF\u27C0-\u27EF\u2980-\u29FF\u2A00-\u2AFF]+|[a-zA-Z$\u00C0-\u00FF\u0370-\u03FF\u0400-\u04FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0590-\u05FF\u0600-\u06FF\u2200-\u22FF\u27C0-\u27EF\u2980-\u29FF\u2A00-\u2AFF][a-zA-Z0-9_$\u00C0-\u00FF\u0370-\u03FF\u0400-\u04FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0590-\u05FF\u0600-\u06FF\u2200-\u22FF\u27C0-\u27EF\u2980-\u29FF\u2A00-\u2AFF]*)/u,
});
// Literals
export const IntegerLiteral = createToken({
name: "IntegerLiteral",
pattern: /-?\d+[lLiIsSbB]?/,
});
// Scientific notation literal (must come before FloatingPointLiteral)
export const ScientificNotationLiteral = createToken({
name: "ScientificNotationLiteral",
pattern: /-?\d+(\.\d+)?[eE][+-]?\d+[fFdD]?/,
});
export const FloatingPointLiteral = createToken({
name: "FloatingPointLiteral",
pattern: /-?\d+\.\d+[fFdD]?|-?\.\d+[fFdD]?/,
});
export const StringLiteral = createToken({
name: "StringLiteral",
pattern: /"""[\s\S]*?"""|"([^"\\]|\\.|\\u[0-9A-Fa-f]{4})*"/,
});
export const InterpolatedStringLiteral = createToken({
name: "InterpolatedStringLiteral",
pattern:
/[a-zA-Z_][a-zA-Z0-9_]*"""[\s\S]*?"""|[a-zA-Z_][a-zA-Z0-9_]*"([^"\\]|\\.|\\u[0-9A-Fa-f]{4}|\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{[^}]*\})*"/,
});
export const CharLiteral = createToken({
name: "CharLiteral",
pattern: /'([^'\\]|\\.|\\u[0-9A-Fa-f]{4})'/,
});
// Operators
export const Equals = createToken({ name: "Equals", pattern: /=/ });
export const Plus = createToken({ name: "Plus", pattern: /\+/ });
export const Minus = createToken({ name: "Minus", pattern: /-/ });
export const Star = createToken({ name: "Star", pattern: /\*/ });
export const Slash = createToken({ name: "Slash", pattern: /\// });
export const Backslash = createToken({ name: "Backslash", pattern: /\\/ });
export const Percent = createToken({ name: "Percent", pattern: /%/ });
export const LessThan = createToken({ name: "LessThan", pattern: /</ });
export const GreaterThan = createToken({ name: "GreaterThan", pattern: />/ });
export const LessThanEquals = createToken({
name: "LessThanEquals",
pattern: /<=/,
});
export const GreaterThanEquals = createToken({
name: "GreaterThanEquals",
pattern: />=/,
});
export const EqualsEquals = createToken({
name: "EqualsEquals",
pattern: /==/,
});
export const DoubleEquals = EqualsEquals; // Alias for modular parser compatibility
export const NotEquals = createToken({ name: "NotEquals", pattern: /!=/ });
export const LogicalAnd = createToken({ name: "LogicalAnd", pattern: /&&/ });
export const LogicalOr = createToken({ name: "LogicalOr", pattern: /\|\|/ });
export const Exclamation = createToken({ name: "Exclamation", pattern: /!/ });
export const Arrow = createToken({ name: "Arrow", pattern: /=>/ });
export const TypeLambdaArrow = createToken({
name: "TypeLambdaArrow",
pattern: /=>>/,
});
export const DoubleArrow = TypeLambdaArrow; // Alias for modular parser compatibility
export const LeftArrow = createToken({ name: "LeftArrow", pattern: /<-/ });
export const RightArrow = createToken({ name: "RightArrow", pattern: /->/ });
export const ContextArrow = createToken({
name: "ContextArrow",
pattern: /\?=>/,
});
export const SubtypeOf = createToken({ name: "SubtypeOf", pattern: /<:/ });
export const ColonLess = SubtypeOf; // Alias for modular parser compatibility
export const SupertypeOf = createToken({ name: "SupertypeOf", pattern: />:/ });
export const GreaterColon = SupertypeOf; // Alias for modular parser compatibility
export const AppendOp = createToken({ name: "AppendOp", pattern: /:\+/ });
export const PlusColon = AppendOp; // Alias for modular parser compatibility
export const ColonPlus = createToken({ name: "ColonPlus", pattern: /:\+/ }); // Same as AppendOp but separate token for parser
export const PrependOp = createToken({ name: "PrependOp", pattern: /::/ });
export const ColonColon = PrependOp; // Alias for modular parser compatibility
export const ConcatOp = createToken({ name: "ConcatOp", pattern: /\+\+/ });
export const DoublePlus = ConcatOp; // Alias for modular parser compatibility
export const AppendEquals = createToken({
name: "AppendEquals",
pattern: /\+\+=/,
});
// Compound assignment operators
export const PlusEquals = createToken({ name: "PlusEquals", pattern: /\+=/ });
export const MinusEquals = createToken({ name: "MinusEquals", pattern: /-=/ });
export const StarEquals = createToken({ name: "StarEquals", pattern: /\*=/ });
export const SlashEquals = createToken({ name: "SlashEquals", pattern: /\/=/ });
export const PercentEquals = createToken({
name: "PercentEquals",
pattern: /%=/,
});
// sbt DSL operators
export const DoublePercent = createToken({
name: "DoublePercent",
pattern: /%%/,
});
// Bitwise operators
export const BitwiseAnd = createToken({ name: "BitwiseAnd", pattern: /&/ });
export const BitwiseOr = createToken({ name: "BitwiseOr", pattern: /\|/ });
export const BitwiseXor = createToken({ name: "BitwiseXor", pattern: /\^/ });
export const BitwiseTilde = createToken({ name: "BitwiseTilde", pattern: /~/ });
export const LeftShift = createToken({ name: "LeftShift", pattern: /<</ });
export const RightShift = createToken({ name: "RightShift", pattern: />>/ });
export const UnsignedRightShift = createToken({
name: "UnsignedRightShift",
pattern: />>>/,
});
export const Colon = createToken({ name: "Colon", pattern: /:/ });
export const ColonEquals = createToken({ name: "ColonEquals", pattern: /:=/ });
export const SbtAssign = ColonEquals; // Alias for sbt compatibility
export const Semicolon = createToken({ name: "Semicolon", pattern: /;/ });
export const Comma = createToken({ name: "Comma", pattern: /,/ });
export const Dot = createToken({ name: "Dot", pattern: /\./ });
export const Underscore = createToken({
name: "Underscore",
pattern: /_/,
});
export const At = createToken({ name: "At", pattern: /@/ });
export const Question = createToken({ name: "Question", pattern: /\?/ });
// Quote and Splice tokens for Scala 3 macros
export const QuoteStart = createToken({ name: "QuoteStart", pattern: /'\{/ });
export const SpliceStart = createToken({
name: "SpliceStart",
pattern: /\$\{/,
});
// Additional tokens for modular parser
export const Quote = createToken({ name: "Quote", pattern: /'/ });
export const Dollar = createToken({ name: "Dollar", pattern: /\$/ });
// QuestionArrow is now alias for ContextArrow to avoid duplicate patterns
export const QuestionArrow = ContextArrow;
// String interpolation tokens
export const InterpolatedString = createToken({
name: "InterpolatedString",
pattern: /s"([^"\\]|\\.|\\u[0-9A-Fa-f]{4})*"/,
});
export const FormattedString = createToken({
name: "FormattedString",
pattern: /f"([^"\\]|\\.|\\u[0-9A-Fa-f]{4})*"/,
});
export const RawString = createToken({
name: "RawString",
pattern: /raw"([^"\\]|\\.|\\u[0-9A-Fa-f]{4})*"/,
});
export const CustomInterpolatedString = createToken({
name: "CustomInterpolatedString",
pattern: /[a-zA-Z_][a-zA-Z0-9_]*"([^"\\]|\\.|\\u[0-9A-Fa-f]{4})*"/,
});
// Numeric suffix tokens
export const LongSuffix = createToken({ name: "LongSuffix", pattern: /[lL]/ });
export const IntSuffix = createToken({ name: "IntSuffix", pattern: /[iI]/ });
export const ShortSuffix = createToken({
name: "ShortSuffix",
pattern: /[sS]/,
});
export const ByteSuffix = createToken({ name: "ByteSuffix", pattern: /[bB]/ });
export const FloatSuffix = createToken({
name: "FloatSuffix",
pattern: /[fF]/,
});
export const DoubleSuffix = createToken({
name: "DoubleSuffix",
pattern: /[dD]/,
});
// Additional missing tokens
export const Hash = createToken({ name: "Hash", pattern: /#/ });
// Delimiters
export const LeftParen = createToken({ name: "LeftParen", pattern: /\(/ });
export const RightParen = createToken({ name: "RightParen", pattern: /\)/ });
export const LeftBracket = createToken({ name: "LeftBracket", pattern: /\[/ });
export const RightBracket = createToken({
name: "RightBracket",
pattern: /\]/,
});
export const LeftBrace = createToken({ name: "LeftBrace", pattern: /\{/ });
export const RightBrace = createToken({ name: "RightBrace", pattern: /\}/ });
// Whitespace and Comments
export const WhiteSpace = createToken({
name: "WhiteSpace",
pattern: /\s+/,
group: Lexer.SKIPPED,
});
export const LineComment = createToken({
name: "LineComment",
pattern: /\/\/[^\n\r]*/,
group: "comments",
});
export const BlockComment = createToken({
name: "BlockComment",
pattern: /\/\*([^*]|\*(?!\/))*\*\//,
group: "comments",
});
// All tokens in order
export const allTokens = [
// Comments (must come before operators)
LineComment,
BlockComment,
// Whitespace
WhiteSpace,
// Keywords (must come before Identifier)
Val,
Var,
Def,
Class,
ObjectKeyword,
Trait,
Extends,
With,
If,
Else,
While,
For,
Yield,
Return,
New,
This,
Super,
Package,
Import,
Case,
Match,
Try,
Catch,
Finally,
Throw,
Null,
True,
False,
NotImplemented,
Type,
Private,
Protected,
Public,
Abstract,
Final,
Sealed,
Implicit,
Lazy,
Override,
Given,
Using,
To,
Enum,
Array,
Extension,
Export,
Opaque,
Inline,
Transparent,
// Literals
ScientificNotationLiteral, // Must come before FloatingPointLiteral
FloatingPointLiteral, // Must come before IntegerLiteral
IntegerLiteral,
// String interpolation literals (must come before StringLiteral)
CustomInterpolatedString,
InterpolatedString,
FormattedString,
RawString,
InterpolatedStringLiteral, // Must come before StringLiteral
StringLiteral,
CharLiteral,
// Multi-character operators (must come before single-character)
TypeLambdaArrow, // Must come before Arrow to avoid ambiguity
ContextArrow, // Must come before Arrow to avoid ambiguity
Arrow,
LeftArrow,
RightArrow,
SubtypeOf,
SupertypeOf,
LessThanEquals,
GreaterThanEquals,
EqualsEquals,
NotEquals,
LogicalAnd,
LogicalOr,
ColonEquals, // := must come before :
AppendOp,
PrependOp,
AppendEquals, // ++= must come before ++
ConcatOp,
// Quote and splice tokens (must come before single-character)
QuoteStart, // '{ must come before single '
SpliceStart, // ${ must come before single $
// Compound assignment operators
PlusEquals,
MinusEquals,
StarEquals,
SlashEquals,
PercentEquals,
// Bitwise shift operators (must come before single-character)
UnsignedRightShift, // >>> must come before >>
LeftShift,
RightShift,
// Single-character operators
Equals,
Plus,
Minus,
Star,
Slash,
Backslash,
DoublePercent, // %% must come before single %
Percent,
LessThan,
GreaterThan,
Exclamation,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
BitwiseTilde,
Colon,
Semicolon,
Comma,
Dot,
At,
// QuestionArrow removed - now an alias for ContextArrow
Question,
Quote,
Dollar,
Hash,
// Delimiters
LeftParen,
RightParen,
LeftBracket,
RightBracket,
LeftBrace,
RightBrace,
// Operator identifier (before regular identifier)
OperatorIdentifier,
// Identifier (must come before underscore)
Identifier,
// Underscore (must come after identifier to not interfere with _identifier patterns)
Underscore,
];
// レキサーの作成(インポート時の問題を回避するための遅延初期化)
let scalaLexerInstance: Lexer | null = null;
/**
* Scalaコードの字句解析を行うレキサー
*/
export const ScalaLexer = {
/**
* レキサーインスタンスを取得(遅延初期化)
* @returns Chevrotainレキサーのインスタンス
*/
get instance(): Lexer {
if (!scalaLexerInstance) {
scalaLexerInstance = new Lexer(allTokens);
}
return scalaLexerInstance;
},
/**
* 入力文字列をトークン化
* @param input - 字句解析対象のScalaソースコード
* @returns トークン化の結果(トークン、エラー、グループ化されたトークン)
*/
tokenize(input: string): ILexingResult {
return this.instance.tokenize(input);
},
};
// Export lexer instance for backward compatibility with tests
export const lexerInstance = ScalaLexer;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
/**
* Base parser module with shared utilities and interfaces
*/
import * as tokens from "../lexer";
import { CstParser, ParserMethod, CstNode } from "chevrotain";
import type { TokenType } from "chevrotain";
export interface ParserRuleMixin {
// Utility methods for parser rules - these need to match CstParser access levels
RULE: CstParser["RULE"];
SUBRULE: CstParser["SUBRULE"];
CONSUME: CstParser["CONSUME"];
MANY: CstParser["MANY"];
MANY_SEP: CstParser["MANY_SEP"];
OPTION: CstParser["OPTION"];
OR: CstParser["OR"];
AT_LEAST_ONE: CstParser["AT_LEAST_ONE"];
AT_LEAST_ONE_SEP: CstParser["AT_LEAST_ONE_SEP"];
LA: CstParser["LA"];
performSelfAnalysis: CstParser["performSelfAnalysis"];
}
export abstract class BaseParserModule {
protected parser: ParserRuleMixin;
constructor(parser: ParserRuleMixin) {
this.parser = parser;
}
// Helper methods for common patterns
protected consumeTokenType(tokenType: TokenType) {
return this.parser.CONSUME(tokenType);
}
protected optionalConsume(tokenType: TokenType) {
return this.parser.OPTION(() => this.parser.CONSUME(tokenType));
}
protected manyOf(rule: () => void) {
return this.parser.MANY(rule);
}
protected oneOf(
alternatives: Array<{ ALT: () => void; GATE?: () => boolean }>,
) {
return this.parser.OR(alternatives);
}
protected subrule(rule: ParserMethod<unknown[], CstNode>) {
return this.parser.SUBRULE(rule);
}
protected lookahead(offset: number) {
return this.parser.LA(offset);
}
}
export { tokens };

View File

@@ -0,0 +1,179 @@
/**
* Definition parsing module for class, object, trait, method, and variable definitions
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class DefinitionParserMixin extends BaseParserModule {
// Dependencies from other modules
annotation!: ParserMethod<unknown[], CstNode>;
modifier!: ParserMethod<unknown[], CstNode>;
typeParameters!: ParserMethod<unknown[], CstNode>;
classParameters!: ParserMethod<unknown[], CstNode>;
extendsClause!: ParserMethod<unknown[], CstNode>;
classBody!: ParserMethod<unknown[], CstNode>;
type!: ParserMethod<unknown[], CstNode>;
expression!: ParserMethod<unknown[], CstNode>;
pattern!: ParserMethod<unknown[], CstNode>;
parameterLists!: ParserMethod<unknown[], CstNode>;
// Class definition
classDefinition = this.parser.RULE("classDefinition", () => {
this.consumeTokenType(tokens.Class);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
// Constructor annotations (for DI patterns like @Inject())
this.manyOf(() => this.subrule(this.annotation));
// Constructor parameters (multiple parameter lists supported)
this.parser.MANY(() => this.subrule(this.classParameters));
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.parser.OPTION(() => this.subrule(this.classBody));
});
// Object definition
objectDefinition = this.parser.RULE("objectDefinition", () => {
this.consumeTokenType(tokens.ObjectKeyword);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.parser.OPTION(() => this.subrule(this.classBody));
});
// Trait definition
traitDefinition = this.parser.RULE("traitDefinition", () => {
this.consumeTokenType(tokens.Trait);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.parser.OPTION(() => this.subrule(this.classBody));
});
// Enum definition (Scala 3)
enumDefinition = this.parser.RULE("enumDefinition", () => {
this.consumeTokenType(tokens.Enum);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.parser.OPTION(() => this.subrule(this.classParameters));
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.consumeTokenType(tokens.LeftBrace);
this.manyOf(() => this.subrule(this.enumCaseDef));
this.consumeTokenType(tokens.RightBrace);
});
enumCaseDef = this.parser.RULE("enumCaseDef", () => {
this.consumeTokenType(tokens.Case);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.classParameters));
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
// Extension definition (Scala 3)
extensionDefinition = this.parser.RULE("extensionDefinition", () => {
this.consumeTokenType(tokens.Extension);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.consumeTokenType(tokens.LeftParen);
this.consumeTokenType(tokens.Identifier);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
this.consumeTokenType(tokens.RightParen);
this.consumeTokenType(tokens.LeftBrace);
this.manyOf(() => this.subrule(this.extensionMemberDef));
this.consumeTokenType(tokens.RightBrace);
});
extensionMemberDef = this.parser.RULE("extensionMemberDef", () => {
this.manyOf(() => this.subrule(this.modifier));
this.subrule(this.defDefinition);
});
// Val definition
valDefinition = this.parser.RULE("valDefinition", () => {
this.consumeTokenType(tokens.Val);
this.oneOf([
{
// Simple variable with optional type: val x: Type = expr or val x: Type (abstract)
ALT: () => {
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
});
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
});
},
GATE: () => {
// This alternative is for simple identifier patterns only
// Must handle: val x = ..., val x: Type = ..., val x: Type (abstract)
// Must NOT handle: val (x, y) = ..., val SomeClass(...) = ...
const first = this.lookahead(1);
const second = this.lookahead(2);
// If first token is not identifier, this is not a simple val
if (!first || first.tokenType !== tokens.Identifier) return false;
// If second token is left paren, this is a constructor pattern
if (second && second.tokenType === tokens.LeftParen) return false;
// Otherwise, this is a simple identifier (with or without type, with or without assignment)
return true;
},
},
{
// Pattern matching: val (x, y) = expr or val SomeClass(...) = expr
ALT: () => {
this.subrule(this.pattern);
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
},
},
]);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
// Var definition
varDefinition = this.parser.RULE("varDefinition", () => {
this.consumeTokenType(tokens.Var);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
});
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
// Method definition
defDefinition = this.parser.RULE("defDefinition", () => {
this.consumeTokenType(tokens.Def);
this.oneOf([
// Regular method name
{ ALT: () => this.consumeTokenType(tokens.Identifier) },
// Constructor (this keyword)
{ ALT: () => this.consumeTokenType(tokens.This) },
]);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.parser.OPTION(() => this.subrule(this.parameterLists));
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
});
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
});
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
// Type definition
typeDefinition = this.parser.RULE("typeDefinition", () => {
this.consumeTokenType(tokens.Type);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.consumeTokenType(tokens.Equals);
this.subrule(this.type);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
}

View File

@@ -0,0 +1,425 @@
/**
* Expression parsing module for all types of expressions in Scala
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class ExpressionParserMixin extends BaseParserModule {
// Dependencies from other modules
annotation!: ParserMethod<unknown[], CstNode>;
modifier!: ParserMethod<unknown[], CstNode>;
type!: ParserMethod<unknown[], CstNode>;
literal!: ParserMethod<unknown[], CstNode>;
qualifiedIdentifier!: ParserMethod<unknown[], CstNode>;
pattern!: ParserMethod<unknown[], CstNode>;
parameterLists!: ParserMethod<unknown[], CstNode>;
typeArgument!: ParserMethod<unknown[], CstNode>;
caseClause!: ParserMethod<unknown[], CstNode>;
generator!: ParserMethod<unknown[], CstNode>;
// Main expression rule
expression = this.parser.RULE("expression", () => {
this.parser.OR([
// Polymorphic function literal (Scala 3)
{
ALT: () => this.subrule(this.polymorphicFunctionLiteral),
GATE: () => {
const la1 = this.parser.LA(1);
return la1?.tokenType === tokens.LeftBracket;
},
},
// Regular expressions
{ ALT: () => this.subrule(this.assignmentOrInfixExpression) },
]);
});
// Assignment or infix expression
assignmentOrInfixExpression = this.parser.RULE(
"assignmentOrInfixExpression",
() => {
this.subrule(this.postfixExpression);
this.parser.MANY(() => {
this.subrule(this.infixOperator);
this.subrule(this.postfixExpression);
});
},
);
// Postfix expression
postfixExpression = this.parser.RULE("postfixExpression", () => {
this.subrule(this.primaryExpression);
this.parser.MANY(() => {
this.parser.OR([
// Method call with parentheses
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.expression),
});
this.consumeTokenType(tokens.RightParen);
},
},
// Type arguments
{
ALT: () => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeArgument),
});
this.consumeTokenType(tokens.RightBracket);
},
},
// Member access
{
ALT: () => {
this.consumeTokenType(tokens.Dot);
this.consumeTokenType(tokens.Identifier);
},
},
// Postfix operator (like Ask pattern ?)
{
ALT: () => {
this.consumeTokenType(tokens.Question);
},
},
]);
});
});
// Primary expression
primaryExpression = this.parser.RULE("primaryExpression", () => {
this.parser.OR([
// Literals
{ ALT: () => this.subrule(this.literal) },
// Identifier
{ ALT: () => this.consumeTokenType(tokens.Identifier) },
// This and super
{ ALT: () => this.consumeTokenType(tokens.This) },
{ ALT: () => this.consumeTokenType(tokens.Super) },
// Underscore (placeholder)
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
// Parenthesized expression
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.OPTION(() => this.subrule(this.expression));
this.consumeTokenType(tokens.RightParen);
},
},
// Block expression
{ ALT: () => this.subrule(this.blockExpression) },
// New expression
{ ALT: () => this.subrule(this.newExpression) },
// Partial function literal
{ ALT: () => this.subrule(this.partialFunctionLiteral) },
// Quote expression (Scala 3)
{ ALT: () => this.subrule(this.quoteExpression) },
// Splice expression (Scala 3)
{ ALT: () => this.subrule(this.spliceExpression) },
// If expression
{ ALT: () => this.subrule(this.ifExpression) },
// While expression
{ ALT: () => this.subrule(this.whileExpression) },
// Try expression
{ ALT: () => this.subrule(this.tryExpression) },
// For expression
{ ALT: () => this.subrule(this.forExpression) },
// Match expression
{
ALT: () => {
this.subrule(this.expression);
this.consumeTokenType(tokens.Match);
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => this.subrule(this.caseClause));
this.consumeTokenType(tokens.RightBrace);
},
},
// Lambda expression
{
ALT: () => {
this.parser.OR([
// Simple identifier lambda: x =>
{
ALT: () => {
this.consumeTokenType(tokens.Identifier);
},
},
// Multiple parameters with optional types: (x, y) =>
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => {
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
});
},
});
this.consumeTokenType(tokens.RightParen);
},
},
]);
this.consumeTokenType(tokens.Arrow);
this.subrule(this.expression);
},
GATE: () => {
const la1 = this.parser.LA(1);
const la2 = this.parser.LA(2);
// Simple lambda: identifier =>
if (
la1?.tokenType === tokens.Identifier &&
la2?.tokenType === tokens.Arrow
) {
return true;
}
// Parenthesized lambda: ( ... ) =>
if (la1?.tokenType === tokens.LeftParen) {
let i = 2;
let parenCount = 1;
while (parenCount > 0 && this.parser.LA(i)) {
const token = this.parser.LA(i);
if (token?.tokenType === tokens.LeftParen) parenCount++;
if (token?.tokenType === tokens.RightParen) parenCount--;
i++;
}
return this.parser.LA(i)?.tokenType === tokens.Arrow;
}
return false;
},
},
]);
});
// Infix operator
infixOperator = this.parser.RULE("infixOperator", () => {
this.parser.OR([
// Special compound assignment operators
{ ALT: () => this.consumeTokenType(tokens.PlusEquals) },
{ ALT: () => this.consumeTokenType(tokens.MinusEquals) },
{ ALT: () => this.consumeTokenType(tokens.StarEquals) },
{ ALT: () => this.consumeTokenType(tokens.SlashEquals) },
{ ALT: () => this.consumeTokenType(tokens.PercentEquals) },
{ ALT: () => this.consumeTokenType(tokens.AppendEquals) },
// sbt-specific operators
{ ALT: () => this.consumeTokenType(tokens.SbtAssign) },
{ ALT: () => this.consumeTokenType(tokens.DoublePercent) },
// Basic operators
{ ALT: () => this.consumeTokenType(tokens.Plus) },
{ ALT: () => this.consumeTokenType(tokens.Minus) },
{ ALT: () => this.consumeTokenType(tokens.Star) },
{ ALT: () => this.consumeTokenType(tokens.Slash) },
{ ALT: () => this.consumeTokenType(tokens.Percent) },
// Comparison operators
{ ALT: () => this.consumeTokenType(tokens.Equals) },
{ ALT: () => this.consumeTokenType(tokens.EqualsEquals) }, // Use EqualsEquals instead of DoubleEquals
{ ALT: () => this.consumeTokenType(tokens.NotEquals) },
{ ALT: () => this.consumeTokenType(tokens.LessThan) }, // Use LessThan instead of Less
{ ALT: () => this.consumeTokenType(tokens.GreaterThan) }, // Use GreaterThan instead of Greater
{ ALT: () => this.consumeTokenType(tokens.LessThanEquals) }, // Use LessThanEquals instead of LessEquals
{ ALT: () => this.consumeTokenType(tokens.GreaterThanEquals) }, // Use GreaterThanEquals instead of GreaterEquals
// Logical operators
{ ALT: () => this.consumeTokenType(tokens.LogicalAnd) }, // Use LogicalAnd instead of DoubleAmpersand
{ ALT: () => this.consumeTokenType(tokens.LogicalOr) }, // Use LogicalOr instead of DoublePipe
// Bitwise operators
{ ALT: () => this.consumeTokenType(tokens.BitwiseAnd) }, // Use BitwiseAnd instead of Ampersand
{ ALT: () => this.consumeTokenType(tokens.BitwiseOr) }, // Use BitwiseOr instead of Pipe
{ ALT: () => this.consumeTokenType(tokens.BitwiseXor) }, // Use BitwiseXor instead of Caret
// Shift operators
{ ALT: () => this.consumeTokenType(tokens.LeftShift) }, // Use LeftShift instead of DoubleLeftAngle
{ ALT: () => this.consumeTokenType(tokens.RightShift) }, // Use RightShift instead of DoubleRightAngle
{ ALT: () => this.consumeTokenType(tokens.UnsignedRightShift) }, // Use UnsignedRightShift instead of TripleRightAngle
// Type operators
{ ALT: () => this.consumeTokenType(tokens.Colon) },
{ ALT: () => this.consumeTokenType(tokens.ColonEquals) },
// Collection operators
{ ALT: () => this.consumeTokenType(tokens.ConcatOp) }, // Use ConcatOp instead of DoublePlus
{ ALT: () => this.consumeTokenType(tokens.PrependOp) }, // Use PrependOp instead of ColonColon
{ ALT: () => this.consumeTokenType(tokens.AppendOp) }, // Use AppendOp instead of ColonPlus/PlusColon
// XML operators
{ ALT: () => this.consumeTokenType(tokens.Backslash) },
// General operator
{ ALT: () => this.consumeTokenType(tokens.OperatorIdentifier) },
// Identifier as operator (for named methods used as infix)
{
ALT: () => this.consumeTokenType(tokens.Identifier),
},
]);
});
// Polymorphic function literal (Scala 3)
polymorphicFunctionLiteral = this.parser.RULE(
"polymorphicFunctionLiteral",
() => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.polymorphicTypeParameter),
});
this.consumeTokenType(tokens.RightBracket);
this.consumeTokenType(tokens.Arrow);
this.subrule(this.expression);
},
);
// New expression
newExpression = this.parser.RULE("newExpression", () => {
this.consumeTokenType(tokens.New);
this.parser.OR([
// New with class instantiation
{
ALT: () => {
this.subrule(this.type);
this.parser.MANY(() => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.expression),
});
this.consumeTokenType(tokens.RightParen);
});
},
},
// New with anonymous class
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
// Class body content
this.consumeTokenType(tokens.RightBrace);
},
},
]);
});
// Block expression
blockExpression = this.parser.RULE("blockExpression", () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => {
this.subrule(this.blockStatement);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
this.consumeTokenType(tokens.RightBrace);
});
// Partial function literal
partialFunctionLiteral = this.parser.RULE("partialFunctionLiteral", () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.AT_LEAST_ONE(() => this.subrule(this.caseClause));
this.consumeTokenType(tokens.RightBrace);
});
// Quote expression (Scala 3)
quoteExpression = this.parser.RULE("quoteExpression", () => {
this.consumeTokenType(tokens.Quote);
this.consumeTokenType(tokens.LeftBrace);
this.subrule(this.expression);
this.consumeTokenType(tokens.RightBrace);
});
// Splice expression (Scala 3)
spliceExpression = this.parser.RULE("spliceExpression", () => {
this.consumeTokenType(tokens.Dollar);
this.consumeTokenType(tokens.LeftBrace);
this.subrule(this.expression);
this.consumeTokenType(tokens.RightBrace);
});
// If expression
ifExpression = this.parser.RULE("ifExpression", () => {
this.consumeTokenType(tokens.If);
this.consumeTokenType(tokens.LeftParen);
this.subrule(this.expression);
this.consumeTokenType(tokens.RightParen);
this.subrule(this.expression);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Else);
this.subrule(this.expression);
});
});
// While expression
whileExpression = this.parser.RULE("whileExpression", () => {
this.consumeTokenType(tokens.While);
this.consumeTokenType(tokens.LeftParen);
this.subrule(this.expression);
this.consumeTokenType(tokens.RightParen);
this.subrule(this.expression);
});
// Try expression
tryExpression = this.parser.RULE("tryExpression", () => {
this.consumeTokenType(tokens.Try);
this.subrule(this.expression);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Catch);
this.parser.OR([
// Pattern-based catch
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => this.subrule(this.caseClause));
this.consumeTokenType(tokens.RightBrace);
},
},
// Expression-based catch
{
ALT: () => this.subrule(this.expression),
},
]);
});
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Finally);
this.subrule(this.expression);
});
});
// For expression/comprehension
forExpression = this.parser.RULE("forExpression", () => {
this.consumeTokenType(tokens.For);
this.parser.OR([
// For with parentheses
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.AT_LEAST_ONE_SEP({
SEP: tokens.Semicolon,
DEF: () => this.subrule(this.generator),
});
this.consumeTokenType(tokens.RightParen);
},
},
// For with braces
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => this.subrule(this.generator));
this.consumeTokenType(tokens.RightBrace);
},
},
]);
this.parser.OPTION(() => this.consumeTokenType(tokens.Yield));
this.subrule(this.expression);
});
// Helper rule dependencies (to be implemented in other modules)
polymorphicTypeParameter = this.parser.RULE(
"polymorphicTypeParameter",
() => {
// Placeholder - should be in types.ts
this.consumeTokenType(tokens.Identifier);
},
);
blockStatement = this.parser.RULE("blockStatement", () => {
// Placeholder - should be in statements.ts
this.subrule(this.expression);
});
}

View File

@@ -0,0 +1,126 @@
/**
* Literal parsing module for all Scala literal types
*/
import { BaseParserModule, tokens } from "./base";
// Module for literal parsing - no additional imports needed
export class LiteralParserMixin extends BaseParserModule {
// Main literal rule
literal = this.parser.RULE("literal", () => {
this.parser.OR([
// Numeric literals
{ ALT: () => this.consumeTokenType(tokens.IntegerLiteral) },
{ ALT: () => this.consumeTokenType(tokens.FloatingPointLiteral) },
{ ALT: () => this.consumeTokenType(tokens.ScientificNotationLiteral) },
// Boolean literals
{ ALT: () => this.consumeTokenType(tokens.True) },
{ ALT: () => this.consumeTokenType(tokens.False) },
// Character literal
{ ALT: () => this.consumeTokenType(tokens.CharLiteral) },
// String literals
{ ALT: () => this.consumeTokenType(tokens.StringLiteral) },
{ ALT: () => this.consumeTokenType(tokens.InterpolatedStringLiteral) },
// Interpolated strings
{ ALT: () => this.subrule(this.interpolatedString) },
// Null literal
{ ALT: () => this.consumeTokenType(tokens.Null) },
// Unit literal ()
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.consumeTokenType(tokens.RightParen);
},
},
]);
});
// Interpolated string
interpolatedString = this.parser.RULE("interpolatedString", () => {
this.parser.OR([
// s-interpolator
{ ALT: () => this.consumeTokenType(tokens.InterpolatedString) },
// f-interpolator
{ ALT: () => this.consumeTokenType(tokens.FormattedString) },
// raw-interpolator
{ ALT: () => this.consumeTokenType(tokens.RawString) },
// Custom interpolator
{ ALT: () => this.consumeTokenType(tokens.CustomInterpolatedString) },
]);
});
// Numeric literal with suffix
numericLiteral = this.parser.RULE("numericLiteral", () => {
this.parser.OR([
// Integer types
{
ALT: () => {
this.consumeTokenType(tokens.IntegerLiteral);
this.parser.OPTION(() => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.LongSuffix) },
{ ALT: () => this.consumeTokenType(tokens.IntSuffix) },
{ ALT: () => this.consumeTokenType(tokens.ShortSuffix) },
{ ALT: () => this.consumeTokenType(tokens.ByteSuffix) },
]);
});
},
},
// Floating point types
{
ALT: () => {
this.consumeTokenType(tokens.FloatingPointLiteral);
this.parser.OPTION(() => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.FloatSuffix) },
{ ALT: () => this.consumeTokenType(tokens.DoubleSuffix) },
]);
});
},
},
]);
});
// XML literal (if XML support is needed)
xmlLiteral = this.parser.RULE("xmlLiteral", () => {
// Placeholder for XML literals
// This would require XML-specific lexing
this.consumeTokenType(tokens.StringLiteral);
});
// Collection literal patterns (syntactic sugar)
collectionLiteral = this.parser.RULE("collectionLiteral", () => {
this.parser.OR([
// List literal: List(1, 2, 3)
{
ALT: () => {
this.consumeTokenType(tokens.Identifier); // List, Set, etc.
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.literal),
});
this.consumeTokenType(tokens.RightParen);
},
},
// Array literal: Array(1, 2, 3)
{
ALT: () => {
this.consumeTokenType(tokens.Array);
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.literal),
});
this.consumeTokenType(tokens.RightParen);
},
},
]);
});
}

View File

@@ -0,0 +1,243 @@
/**
* Pattern matching parsing module
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class PatternParserMixin extends BaseParserModule {
// Dependencies from other modules
literal!: ParserMethod<unknown[], CstNode>;
qualifiedIdentifier!: ParserMethod<unknown[], CstNode>;
type!: ParserMethod<unknown[], CstNode>;
expression!: ParserMethod<unknown[], CstNode>;
// Pattern rule
pattern = this.parser.RULE("pattern", () => {
this.parser.OR([
// Wildcard pattern: _
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
// Literal pattern
{ ALT: () => this.subrule(this.literal) },
// Variable pattern (lowercase identifier)
{
ALT: () => this.consumeTokenType(tokens.Identifier),
GATE: () => {
const la1 = this.parser.LA(1);
if (la1?.tokenType !== tokens.Identifier) return false;
const firstChar = la1.image[0];
return (
firstChar === firstChar.toLowerCase() &&
firstChar !== firstChar.toUpperCase()
);
},
},
// Stable identifier pattern (uppercase or qualified)
{
ALT: () => this.subrule(this.qualifiedIdentifier),
},
// Constructor pattern: Type(patterns...)
{
ALT: () => {
this.subrule(this.qualifiedIdentifier);
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.pattern),
});
this.consumeTokenType(tokens.RightParen);
},
GATE: () => {
// Look for Constructor(...)
let i = 1;
while (this.parser.LA(i)?.tokenType === tokens.Identifier) {
if (this.parser.LA(i + 1)?.tokenType === tokens.Dot) {
i += 2;
} else {
return this.parser.LA(i + 1)?.tokenType === tokens.LeftParen;
}
}
return false;
},
},
// Tuple pattern: (p1, p2, ...)
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.pattern),
});
this.consumeTokenType(tokens.RightParen);
},
},
// Typed pattern: pattern : Type
{
ALT: () => {
this.subrule(this.pattern);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
},
GATE: () => {
// Complex lookahead for typed patterns
let i = 1;
let parenDepth = 0;
while (i < 20) {
const token = this.parser.LA(i);
if (!token) return false;
if (token.tokenType === tokens.LeftParen) parenDepth++;
if (token.tokenType === tokens.RightParen) parenDepth--;
if (parenDepth === 0 && token.tokenType === tokens.Colon) {
return true;
}
if (
parenDepth === 0 &&
(token.tokenType === tokens.Arrow ||
token.tokenType === tokens.Equals ||
token.tokenType === tokens.If)
) {
return false;
}
i++;
}
return false;
},
},
// Alternative pattern: p1 | p2 | ...
{
ALT: () => {
this.subrule(this.pattern);
this.parser.MANY(() => {
this.consumeTokenType(tokens.BitwiseOr);
this.subrule(this.pattern);
});
},
GATE: () => {
// Look for | in patterns
let i = 1;
let parenDepth = 0;
while (i < 20) {
const token = this.parser.LA(i);
if (!token) return false;
if (token.tokenType === tokens.LeftParen) parenDepth++;
if (token.tokenType === tokens.RightParen) parenDepth--;
if (parenDepth === 0 && token.tokenType === tokens.BitwiseOr) {
return true;
}
if (
parenDepth === 0 &&
(token.tokenType === tokens.Arrow ||
token.tokenType === tokens.Equals)
) {
return false;
}
i++;
}
return false;
},
},
]);
});
// Case clause (used in match expressions and partial functions)
caseClause = this.parser.RULE("caseClause", () => {
this.consumeTokenType(tokens.Case);
this.subrule(this.pattern);
// Optional guard
this.parser.OPTION(() => {
this.consumeTokenType(tokens.If);
this.subrule(this.expression);
});
this.consumeTokenType(tokens.Arrow);
// Case body - can be expression or block
this.parser.OR([
// Block of statements
{
ALT: () => {
this.parser.MANY(() => {
this.subrule(this.expression);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
},
GATE: () => {
// If next token is 'case' or '}', this is the end
const la1 = this.parser.LA(1);
return (
la1?.tokenType !== tokens.Case &&
la1?.tokenType !== tokens.RightBrace
);
},
},
// Empty case (rare but valid)
{ ALT: () => {} },
]);
});
// Generator (used in for comprehensions)
generator = this.parser.RULE("generator", () => {
this.parser.OR([
// Pattern generator: pattern <- expression
{
ALT: () => {
this.subrule(this.pattern);
this.consumeTokenType(tokens.LeftArrow);
this.subrule(this.expression);
},
},
// Value definition: pattern = expression
{
ALT: () => {
this.subrule(this.pattern);
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
},
},
// Guard: if expression
{
ALT: () => {
this.consumeTokenType(tokens.If);
this.subrule(this.expression);
},
},
]);
});
// Extractor pattern (for advanced pattern matching)
extractorPattern = this.parser.RULE("extractorPattern", () => {
this.subrule(this.qualifiedIdentifier);
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => {
this.parser.OR([
// Regular pattern
{ ALT: () => this.subrule(this.pattern) },
// Sequence pattern: _*
{
ALT: () => {
this.consumeTokenType(tokens.Underscore);
this.consumeTokenType(tokens.Star);
},
},
]);
},
});
this.consumeTokenType(tokens.RightParen);
});
// Infix pattern (for pattern matching with infix operators)
infixPattern = this.parser.RULE("infixPattern", () => {
this.subrule(this.pattern);
this.consumeTokenType(tokens.Identifier);
this.subrule(this.pattern);
});
// XML pattern (if XML support is needed)
xmlPattern = this.parser.RULE("xmlPattern", () => {
// Placeholder for XML patterns
// This would require XML-specific tokens
this.consumeTokenType(tokens.StringLiteral);
});
}

View File

@@ -0,0 +1,298 @@
/**
* Scala 3 specific features parsing module
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class Scala3ParserMixin extends BaseParserModule {
// Dependencies from other modules
annotation!: ParserMethod<unknown[], CstNode>;
modifier!: ParserMethod<unknown[], CstNode>;
typeParameters!: ParserMethod<unknown[], CstNode>;
type!: ParserMethod<unknown[], CstNode>;
expression!: ParserMethod<unknown[], CstNode>;
pattern!: ParserMethod<unknown[], CstNode>;
parameterLists!: ParserMethod<unknown[], CstNode>;
classBody!: ParserMethod<unknown[], CstNode>;
extendsClause!: ParserMethod<unknown[], CstNode>;
qualifiedIdentifier!: ParserMethod<unknown[], CstNode>;
valDefinition!: ParserMethod<unknown[], CstNode>;
defDefinition!: ParserMethod<unknown[], CstNode>;
typeDefinition!: ParserMethod<unknown[], CstNode>;
// Enum definition (Scala 3)
enumDefinition = this.parser.RULE("enumDefinition", () => {
this.consumeTokenType(tokens.Enum);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
this.parser.OPTION(() => this.subrule(this.extendsClause));
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => {
this.parser.OR([
{ ALT: () => this.subrule(this.enumCase) },
{ ALT: () => this.subrule(this.classMember) },
]);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
this.consumeTokenType(tokens.RightBrace);
});
// Enum case
enumCase = this.parser.RULE("enumCase", () => {
this.consumeTokenType(tokens.Case);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => {
this.consumeTokenType(tokens.Identifier);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
},
});
this.consumeTokenType(tokens.RightParen);
});
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Extends);
this.subrule(this.type);
});
});
// Extension definition (Scala 3)
extensionDefinition = this.parser.RULE("extensionDefinition", () => {
this.consumeTokenType(tokens.Extension);
// Optional type parameters before the extended type
this.parser.OPTION(() => this.subrule(this.typeParameters));
// Extended type with parameters
this.consumeTokenType(tokens.LeftParen);
this.consumeTokenType(tokens.Identifier);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
this.consumeTokenType(tokens.RightParen);
// Optional using/given clauses
this.parser.MANY(() => this.subrule(this.parameterLists));
// Extension body
this.parser.OR([
// Single method
{ ALT: () => this.subrule(this.extensionMember) },
// Multiple methods in braces
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => {
this.subrule(this.extensionMember);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
this.consumeTokenType(tokens.RightBrace);
},
},
]);
});
// Extension member
extensionMember = this.parser.RULE("extensionMember", () => {
this.parser.MANY(() => this.subrule(this.annotation));
this.parser.MANY(() => this.subrule(this.modifier));
this.parser.OR([
{ ALT: () => this.subrule(this.defDefinition) },
{ ALT: () => this.subrule(this.valDefinition) },
{ ALT: () => this.subrule(this.typeDefinition) },
]);
});
// Given definition (Scala 3)
givenDefinition = this.parser.RULE("givenDefinition", () => {
this.consumeTokenType(tokens.Given);
// Optional given name
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Identifier);
});
// Optional type parameters
this.parser.OPTION(() => this.subrule(this.typeParameters));
// Optional parameter lists (for given with parameters)
this.parser.MANY(() => this.subrule(this.parameterLists));
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
// Implementation
this.parser.OR([
// With implementation
{
ALT: () => {
this.consumeTokenType(tokens.With);
this.parser.OR([
// Block implementation
{ ALT: () => this.subrule(this.classBody) },
// Expression implementation
{
ALT: () => {
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
},
},
]);
},
},
// Direct implementation with =
{
ALT: () => {
this.consumeTokenType(tokens.Equals);
this.subrule(this.expression);
},
},
]);
});
// Opaque type definition (Scala 3)
opaqueTypeDefinition = this.parser.RULE("opaqueTypeDefinition", () => {
this.consumeTokenType(tokens.Opaque);
this.consumeTokenType(tokens.Type);
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => this.subrule(this.typeParameters));
// Optional type bounds
this.parser.OPTION(() => {
this.parser.OR([
{
ALT: () => {
this.consumeTokenType(tokens.ColonLess);
this.subrule(this.type);
},
},
{
ALT: () => {
this.consumeTokenType(tokens.GreaterColon);
this.subrule(this.type);
},
},
]);
});
this.consumeTokenType(tokens.Equals);
this.subrule(this.type);
});
// Inline modifier handling (Scala 3)
inlineDefinition = this.parser.RULE("inlineDefinition", () => {
this.consumeTokenType(tokens.Inline);
this.parser.OR([
{
ALT: () => this.subrule(this.defDefinition),
},
{
ALT: () => this.subrule(this.valDefinition),
},
]);
});
// Transparent modifier handling (Scala 3)
transparentDefinition = this.parser.RULE("transparentDefinition", () => {
this.consumeTokenType(tokens.Transparent);
this.consumeTokenType(tokens.Inline);
this.subrule(this.defDefinition);
});
// Export clause (already implemented in statements, but Scala 3 specific)
// Moved from statements module for better organization
exportClause = this.parser.RULE("exportClause", () => {
this.consumeTokenType(tokens.Export);
this.subrule(this.exportExpression);
this.parser.OPTION(() => this.consumeTokenType(tokens.Semicolon));
});
exportExpression = this.parser.RULE("exportExpression", () => {
this.subrule(this.qualifiedIdentifier);
this.consumeTokenType(tokens.Dot);
this.parser.MANY(() => {
this.consumeTokenType(tokens.Dot);
this.parser.OR([
{
ALT: () => this.consumeTokenType(tokens.Identifier),
},
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
{ ALT: () => this.consumeTokenType(tokens.Given) },
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.exportSelector),
});
this.consumeTokenType(tokens.RightBrace);
},
},
]);
});
});
exportSelector = this.parser.RULE("exportSelector", () => {
this.parser.OR([
// given selector
{ ALT: () => this.consumeTokenType(tokens.Given) },
// Regular selector with optional rename
{
ALT: () => {
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.parser.OR([
// Rename: x => y
{
ALT: () => {
this.consumeTokenType(tokens.Arrow);
this.consumeTokenType(tokens.Identifier);
},
},
// Hide: x => _
{
ALT: () => {
this.consumeTokenType(tokens.Arrow);
this.consumeTokenType(tokens.Underscore);
},
},
]);
});
},
},
]);
});
// Using clause (Scala 3 - for context parameters)
usingClause = this.parser.RULE("usingClause", () => {
this.consumeTokenType(tokens.Using);
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => {
this.consumeTokenType(tokens.Identifier);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
},
});
this.consumeTokenType(tokens.RightParen);
});
// Helper rule placeholder
classMember = this.parser.RULE("classMember", () => {
// Placeholder - should be in definitions.ts
this.parser.OR([
{
ALT: () => this.subrule(this.valDefinition),
},
{
ALT: () => this.subrule(this.defDefinition),
},
{
ALT: () => this.subrule(this.typeDefinition),
},
]);
});
}

View File

@@ -0,0 +1,165 @@
/**
* Statement parsing module for package, import, and export declarations
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class StatementParserMixin extends BaseParserModule {
// Dependencies from other modules
qualifiedIdentifier!: ParserMethod<unknown[], CstNode>;
expression!: ParserMethod<unknown[], CstNode>;
// Package declaration
packageClause = this.parser.RULE("packageClause", () => {
this.consumeTokenType(tokens.Package);
this.subrule(this.qualifiedIdentifier);
this.optionalConsume(tokens.Semicolon);
});
// Import declaration
importClause = this.parser.RULE("importClause", () => {
this.consumeTokenType(tokens.Import);
this.subrule(this.importExpression);
this.optionalConsume(tokens.Semicolon);
});
importExpression = this.parser.RULE("importExpression", () => {
// Parse the base path (e.g., "scala.collection")
this.consumeTokenType(tokens.Identifier);
this.manyOf(() => {
this.consumeTokenType(tokens.Dot);
this.oneOf([
// Next identifier in path
{
ALT: () =>
this.parser.CONSUME(tokens.Identifier, { LABEL: "Identifier2" }),
},
// Wildcard import
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
// Multiple import selectors
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.AT_LEAST_ONE_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.importSelector),
});
this.consumeTokenType(tokens.RightBrace);
},
},
]);
});
});
importSelector = this.parser.RULE("importSelector", () => {
this.oneOf([
{
ALT: () => {
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Arrow);
this.oneOf([
{
ALT: () =>
this.parser.CONSUME(tokens.Identifier, {
LABEL: "Identifier2",
}),
},
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
]);
});
},
},
{
ALT: () =>
this.parser.CONSUME(tokens.Underscore, { LABEL: "Underscore2" }),
}, // Allow wildcard import in selectors
]);
});
// Export declaration (Scala 3)
exportClause = this.parser.RULE("exportClause", () => {
this.consumeTokenType(tokens.Export);
this.subrule(this.exportExpression);
this.optionalConsume(tokens.Semicolon);
});
exportExpression = this.parser.RULE("exportExpression", () => {
// Parse the base path (e.g., "mypackage")
this.consumeTokenType(tokens.Identifier);
this.manyOf(() => {
this.consumeTokenType(tokens.Dot);
this.oneOf([
// Next identifier in path
{
ALT: () =>
this.parser.CONSUME(tokens.Identifier, { LABEL: "Identifier2" }),
},
// Given keyword for given exports
{ ALT: () => this.consumeTokenType(tokens.Given) },
// Wildcard export
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
// Multiple export selectors
{
ALT: () => {
this.consumeTokenType(tokens.LeftBrace);
this.parser.AT_LEAST_ONE_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.exportSelector),
});
this.consumeTokenType(tokens.RightBrace);
},
},
]);
});
});
exportSelector = this.parser.RULE("exportSelector", () => {
this.oneOf([
{
ALT: () => {
this.consumeTokenType(tokens.Identifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.Arrow);
this.oneOf([
{
ALT: () =>
this.parser.CONSUME(tokens.Identifier, {
LABEL: "Identifier2",
}),
},
{ ALT: () => this.consumeTokenType(tokens.Underscore) },
]);
});
},
},
{
ALT: () =>
this.parser.CONSUME(tokens.Underscore, { LABEL: "Underscore2" }),
},
{ ALT: () => this.consumeTokenType(tokens.Given) },
{
ALT: () => {
this.consumeTokenType(tokens.Given);
this.consumeTokenType(tokens.Identifier);
},
},
]);
});
// Assignment statement (for sbt files and general assignments)
assignmentStatement = this.parser.RULE("assignmentStatement", () => {
this.consumeTokenType(tokens.Identifier);
this.oneOf([
{ ALT: () => this.consumeTokenType(tokens.SbtAssign) },
{ ALT: () => this.consumeTokenType(tokens.PlusEquals) },
{ ALT: () => this.consumeTokenType(tokens.MinusEquals) },
{ ALT: () => this.consumeTokenType(tokens.StarEquals) },
{ ALT: () => this.consumeTokenType(tokens.SlashEquals) },
{ ALT: () => this.consumeTokenType(tokens.PercentEquals) },
{ ALT: () => this.consumeTokenType(tokens.AppendEquals) },
{ ALT: () => this.consumeTokenType(tokens.Equals) },
]);
this.subrule(this.expression);
});
}

View File

@@ -0,0 +1,439 @@
/**
* Type system parsing module for Scala types
*/
import { BaseParserModule, tokens } from "./base";
import type { ParserMethod, CstNode } from "chevrotain";
export class TypeParserMixin extends BaseParserModule {
// Dependencies from other modules
qualifiedIdentifier!: ParserMethod<unknown[], CstNode>;
expression!: ParserMethod<unknown[], CstNode>;
literal!: ParserMethod<unknown[], CstNode>;
// Main type rule
type = this.parser.RULE("type", () => {
this.subrule(this.unionType);
});
// Union types (Scala 3)
unionType = this.parser.RULE("unionType", () => {
this.subrule(this.intersectionType);
this.parser.MANY(() => {
this.consumeTokenType(tokens.BitwiseOr);
this.subrule(this.intersectionType);
});
});
// Intersection types (Scala 3)
intersectionType = this.parser.RULE("intersectionType", () => {
this.subrule(this.baseType);
this.parser.MANY(() => {
this.consumeTokenType(tokens.BitwiseAnd);
this.subrule(this.baseType);
});
});
// Base type
baseType = this.parser.RULE("baseType", () => {
this.parser.OR([
// Simple type
{ ALT: () => this.subrule(this.simpleType) },
// Function type: A => B or (A, B) => C
{
ALT: () => {
this.parser.OR([
// Single parameter without parentheses
{
ALT: () => this.subrule(this.simpleType),
},
// Multiple parameters or single with parentheses
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.type),
});
this.consumeTokenType(tokens.RightParen);
},
},
]);
this.consumeTokenType(tokens.Arrow);
this.subrule(this.type);
},
GATE: () => {
// Look ahead to detect function types
let i = 1;
const la1 = this.parser.LA(i);
// Simple function type: Type =>
if (la1?.tokenType === tokens.Identifier) {
const la2 = this.parser.LA(2);
if (la2?.tokenType === tokens.Arrow) return true;
if (la2?.tokenType === tokens.Dot) {
// Handle qualified types like A.B =>
i = 3;
while (
this.parser.LA(i)?.tokenType === tokens.Identifier &&
this.parser.LA(i + 1)?.tokenType === tokens.Dot
) {
i += 2;
}
return this.parser.LA(i + 1)?.tokenType === tokens.Arrow;
}
}
// Parenthesized function type: (...) =>
if (la1?.tokenType === tokens.LeftParen) {
let parenCount = 1;
i = 2;
while (parenCount > 0 && i < 50) {
const token = this.parser.LA(i);
if (token?.tokenType === tokens.LeftParen) parenCount++;
if (token?.tokenType === tokens.RightParen) parenCount--;
i++;
}
return this.parser.LA(i)?.tokenType === tokens.Arrow;
}
return false;
},
},
// Context function type (Scala 3): A ?=> B
{
ALT: () => this.subrule(this.contextFunctionType),
GATE: () => {
// Look for ?=> pattern
let i = 1;
while (i < 20) {
const token = this.parser.LA(i);
if (token?.tokenType === tokens.QuestionArrow) return true;
if (!token) return false;
i++;
}
return false;
},
},
// Dependent function type (Scala 3)
{
ALT: () => this.subrule(this.dependentFunctionType),
GATE: () => {
const la1 = this.parser.LA(1);
const la2 = this.parser.LA(2);
const la3 = this.parser.LA(3);
return (
la1?.tokenType === tokens.LeftParen &&
la2?.tokenType === tokens.Identifier &&
la3?.tokenType === tokens.Colon
);
},
},
// Polymorphic function type (Scala 3): [T] => T => T
{
ALT: () => this.subrule(this.polymorphicFunctionType),
GATE: () => {
const la1 = this.parser.LA(1);
if (la1?.tokenType !== tokens.LeftBracket) return false;
// Look for ] =>> pattern
let i = 2;
let bracketCount = 1;
while (bracketCount > 0 && i < 30) {
const token = this.parser.LA(i);
if (token?.tokenType === tokens.LeftBracket) bracketCount++;
if (token?.tokenType === tokens.RightBracket) bracketCount--;
i++;
}
return this.parser.LA(i)?.tokenType === tokens.DoubleArrow;
},
},
]);
});
// Simple type
simpleType = this.parser.RULE("simpleType", () => {
this.parser.OR([
// Literal type
{
ALT: () => this.subrule(this.literal),
GATE: () => {
const la1 = this.parser.LA(1);
return (
la1?.tokenType === tokens.IntegerLiteral ||
la1?.tokenType === tokens.FloatingPointLiteral ||
la1?.tokenType === tokens.True ||
la1?.tokenType === tokens.CharLiteral ||
la1?.tokenType === tokens.StringLiteral ||
la1?.tokenType === tokens.Null
);
},
},
// Tuple type or parenthesized type
{ ALT: () => this.subrule(this.tupleTypeOrParenthesized) },
// Type projection: T#U
{
ALT: () => {
this.subrule(this.simpleType);
this.consumeTokenType(tokens.Hash);
this.consumeTokenType(tokens.Identifier);
},
GATE: () => {
// Complex lookahead for type projection
let i = 1;
while (i < 20) {
const token = this.parser.LA(i);
if (token?.tokenType === tokens.Hash) return true;
if (
!token ||
token.tokenType === tokens.Arrow ||
token.tokenType === tokens.Comma
)
return false;
i++;
}
return false;
},
},
// Singleton type: x.type
{
ALT: () => {
this.subrule(this.qualifiedIdentifier);
this.consumeTokenType(tokens.Dot);
this.consumeTokenType(tokens.Type);
},
GATE: () => {
let i = 1;
while (
this.parser.LA(i)?.tokenType === tokens.Identifier &&
this.parser.LA(i + 1)?.tokenType === tokens.Dot
) {
i += 2;
}
return (
this.parser.LA(i)?.tokenType === tokens.Identifier &&
this.parser.LA(i + 1)?.tokenType === tokens.Dot &&
this.parser.LA(i + 2)?.tokenType === tokens.Type
);
},
},
// Wildcard type: _
{
ALT: () => this.consumeTokenType(tokens.Underscore),
},
// Kind projector: * or ?
{
ALT: () => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.Star) },
{ ALT: () => this.consumeTokenType(tokens.Question) },
]);
},
},
// Array type constructor
{
ALT: () => {
this.consumeTokenType(tokens.Array);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeArgument),
});
this.consumeTokenType(tokens.RightBracket);
});
},
},
// Regular type with optional type arguments
{
ALT: () => {
this.subrule(this.qualifiedIdentifier);
this.parser.OPTION(() => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeArgument),
});
this.consumeTokenType(tokens.RightBracket);
});
},
},
]);
});
// Type argument
typeArgument = this.parser.RULE("typeArgument", () => {
// Optional variance annotation
this.parser.OPTION(() => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.Plus) },
{ ALT: () => this.consumeTokenType(tokens.Minus) },
]);
});
this.subrule(this.type);
});
// Tuple type or parenthesized type
tupleTypeOrParenthesized = this.parser.RULE(
"tupleTypeOrParenthesized",
() => {
this.consumeTokenType(tokens.LeftParen);
this.parser.OPTION(() => {
this.subrule(this.type);
this.parser.MANY(() => {
this.consumeTokenType(tokens.Comma);
this.subrule(this.type);
});
});
this.consumeTokenType(tokens.RightParen);
},
);
// Context function type (Scala 3)
contextFunctionType = this.parser.RULE("contextFunctionType", () => {
this.parser.OR([
// Single parameter
{ ALT: () => this.subrule(this.simpleType) },
// Multiple parameters
{
ALT: () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.type),
});
this.consumeTokenType(tokens.RightParen);
},
},
]);
this.consumeTokenType(tokens.QuestionArrow);
this.subrule(this.type);
});
// Dependent function type (Scala 3)
dependentFunctionType = this.parser.RULE("dependentFunctionType", () => {
this.consumeTokenType(tokens.LeftParen);
this.parser.AT_LEAST_ONE_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.dependentParameter),
});
this.consumeTokenType(tokens.RightParen);
this.consumeTokenType(tokens.Arrow);
this.subrule(this.type);
});
// Dependent parameter
dependentParameter = this.parser.RULE("dependentParameter", () => {
this.consumeTokenType(tokens.Identifier);
this.consumeTokenType(tokens.Colon);
this.subrule(this.type);
});
// Polymorphic function type (Scala 3)
polymorphicFunctionType = this.parser.RULE("polymorphicFunctionType", () => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeLambdaParameter),
});
this.consumeTokenType(tokens.RightBracket);
this.consumeTokenType(tokens.DoubleArrow);
this.subrule(this.type);
});
// Type lambda (Scala 3)
typeLambda = this.parser.RULE("typeLambda", () => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeLambdaParameter),
});
this.consumeTokenType(tokens.RightBracket);
this.consumeTokenType(tokens.DoubleArrow);
this.subrule(this.type);
});
// Type lambda parameter
typeLambdaParameter = this.parser.RULE("typeLambdaParameter", () => {
// Optional variance
this.parser.OPTION(() => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.Plus) },
{ ALT: () => this.consumeTokenType(tokens.Minus) },
]);
});
this.consumeTokenType(tokens.Identifier);
// Optional type bounds
this.parser.OPTION(() => {
this.parser.OR([
{
ALT: () => {
this.consumeTokenType(tokens.ColonLess);
this.subrule(this.type);
},
},
{
ALT: () => {
this.consumeTokenType(tokens.GreaterColon);
this.subrule(this.type);
},
},
]);
});
});
// Type parameters
typeParameters = this.parser.RULE("typeParameters", () => {
this.consumeTokenType(tokens.LeftBracket);
this.parser.MANY_SEP({
SEP: tokens.Comma,
DEF: () => this.subrule(this.typeParameter),
});
this.consumeTokenType(tokens.RightBracket);
});
// Type parameter
typeParameter = this.parser.RULE("typeParameter", () => {
// Optional variance annotation
this.parser.OPTION(() => {
this.parser.OR([
{ ALT: () => this.consumeTokenType(tokens.Plus) },
{ ALT: () => this.consumeTokenType(tokens.Minus) },
]);
});
this.consumeTokenType(tokens.Identifier);
// Optional type bounds
this.parser.OPTION(() => {
this.parser.OR([
{
ALT: () => {
this.consumeTokenType(tokens.ColonLess);
this.subrule(this.type);
},
},
{
ALT: () => {
this.consumeTokenType(tokens.GreaterColon);
this.subrule(this.type);
},
},
]);
});
});
// Match type (Scala 3)
matchType = this.parser.RULE("matchType", () => {
this.subrule(this.type);
this.consumeTokenType(tokens.Match);
this.consumeTokenType(tokens.LeftBrace);
this.parser.MANY(() => this.subrule(this.matchTypeCase));
this.consumeTokenType(tokens.RightBrace);
});
// Match type case
matchTypeCase = this.parser.RULE("matchTypeCase", () => {
this.consumeTokenType(tokens.Case);
this.subrule(this.type);
this.consumeTokenType(tokens.Arrow);
this.subrule(this.type);
});
}

View File

@@ -0,0 +1,63 @@
import type {
CstNode,
IToken,
ILexingError,
IRecognitionException,
CstElement,
} from "chevrotain";
export interface SourceLocation {
startOffset: number;
endOffset: number;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
}
export interface ScalaCstNode extends CstNode {
name: string;
children: Record<string, CstElement[]>;
location?: SourceLocation;
// Additional properties for compatibility
image?: string;
type?: string;
originalComments?: string[];
startLine?: number;
value?: string;
startOffset?: number;
endOffset?: number;
}
export interface ParseResult {
cst: ScalaCstNode;
errors: IRecognitionException[];
comments: IToken[];
}
export interface LexResult {
tokens: IToken[];
errors: ILexingError[];
groups: {
comments?: IToken[];
};
}
export interface TokenBounds {
start: number;
end: number;
}
export interface LineColumn {
line: number;
column: number;
}
// Chevrotain パーサーメソッドの戻り値型
export interface ParserMethodResult extends CstNode {
name: string;
children: Record<string, (CstNode | IToken)[]>;
}
// パーサールールの型定義
export type ParserRule<T = ParserMethodResult> = () => T;

View File

@@ -0,0 +1,182 @@
/**
* Unicode utilities for Scala parser
* Handles Unicode normalization and character validation
*/
/**
* Normalizes Unicode strings using NFC (Canonical Decomposition, followed by Canonical Composition)
* This ensures consistent representation of Unicode characters.
*
* @param text - The input text to normalize
* @returns The normalized text
*/
export function normalizeUnicode(text: string): string {
return text.normalize("NFC");
}
/**
* Checks if a character is a valid Scala identifier start character
* Follows Unicode identifier specification for Scala
*
* @param char - The character to check
* @returns True if the character can start an identifier
*/
export function isIdentifierStart(char: string): boolean {
if (char.length !== 1) return false;
const codePoint = char.codePointAt(0);
if (codePoint === undefined) return false;
// Basic ASCII identifier characters
if (
(codePoint >= 0x41 && codePoint <= 0x5a) || // A-Z
(codePoint >= 0x61 && codePoint <= 0x7a) || // a-z
codePoint === 0x5f || // _
codePoint === 0x24
) {
// $
return true;
}
// Mathematical symbols range (extended)
if (
(codePoint >= 0x2200 && codePoint <= 0x22ff) || // Mathematical Operators
(codePoint >= 0x27c0 && codePoint <= 0x27ef) || // Miscellaneous Mathematical Symbols-A
(codePoint >= 0x2980 && codePoint <= 0x29ff) || // Miscellaneous Mathematical Symbols-B
(codePoint >= 0x2a00 && codePoint <= 0x2aff)
) {
// Supplemental Mathematical Operators
return true;
}
// Use Unicode property test for other characters (excluding digits for start characters)
const testRegex = /\p{L}|\p{Mn}|\p{Mc}|\p{Pc}/u;
return testRegex.test(char);
}
/**
* Checks if a character is a valid Scala identifier continuation character
*
* @param char - The character to check
* @returns True if the character can continue an identifier
*/
export function isIdentifierContinue(char: string): boolean {
if (char.length !== 1) return false;
const codePoint = char.codePointAt(0);
if (codePoint === undefined) return false;
// Basic ASCII identifier characters
if (
(codePoint >= 0x41 && codePoint <= 0x5a) || // A-Z
(codePoint >= 0x61 && codePoint <= 0x7a) || // a-z
(codePoint >= 0x30 && codePoint <= 0x39) || // 0-9
codePoint === 0x5f || // _
codePoint === 0x24
) {
// $
return true;
}
// Mathematical symbols range (extended)
if (
(codePoint >= 0x2200 && codePoint <= 0x22ff) || // Mathematical Operators
(codePoint >= 0x27c0 && codePoint <= 0x27ef) || // Miscellaneous Mathematical Symbols-A
(codePoint >= 0x2980 && codePoint <= 0x29ff) || // Miscellaneous Mathematical Symbols-B
(codePoint >= 0x2a00 && codePoint <= 0x2aff)
) {
// Supplemental Mathematical Operators
return true;
}
// Use Unicode property test for other characters (including format characters)
const testRegex = /\p{L}|\p{Mn}|\p{Mc}|\p{Nd}|\p{Pc}|\p{Cf}/u;
return testRegex.test(char);
}
/**
* Validates that a string is a valid Scala identifier
*
* @param identifier - The identifier string to validate
* @returns True if the string is a valid identifier
*/
export function isValidIdentifier(identifier: string): boolean {
if (!identifier || identifier.length === 0) return false;
// Normalize the identifier
const normalized = normalizeUnicode(identifier);
// Check first character
if (!isIdentifierStart(normalized[0])) return false;
// Check remaining characters
for (let i = 1; i < normalized.length; i++) {
if (!isIdentifierContinue(normalized[i])) return false;
}
return true;
}
/**
* Converts Unicode escape sequences in strings to actual Unicode characters
* Handles \uXXXX patterns in string literals
*
* @param text - The text containing Unicode escapes
* @returns The text with Unicode escapes converted to actual characters
*/
export function processUnicodeEscapes(text: string): string {
return text.replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) => {
const codePoint = parseInt(hex, 16);
return String.fromCharCode(codePoint);
});
}
/**
* Escapes Unicode characters in strings for safe output
* Converts non-ASCII characters back to \uXXXX format if needed
*
* @param text - The text to escape
* @param escapeNonAscii - Whether to escape all non-ASCII characters
* @returns The escaped text
*/
export function escapeUnicode(text: string, escapeNonAscii = false): string {
if (!escapeNonAscii) return text;
return text.replace(/[\u0080-\uFFFF]/g, (char) => {
const codePoint = char.charCodeAt(0);
return `\\u${codePoint.toString(16).padStart(4, "0").toUpperCase()}`;
});
}
/**
* Extended mathematical symbols commonly used in Scala functional programming
*/
export const MATHEMATICAL_SYMBOLS = {
// Greek letters commonly used in functional programming
ALPHA: "α", // U+03B1
BETA: "β", // U+03B2
GAMMA: "γ", // U+03B3
DELTA: "δ", // U+03B4
LAMBDA: "λ", // U+03BB
MU: "μ", // U+03BC
PI: "π", // U+03C0
SIGMA: "σ", // U+03C3
TAU: "τ", // U+03C4
PHI: "φ", // U+03C6
// Mathematical operators
FORALL: "∀", // U+2200
EXISTS: "∃", // U+2203
ELEMENT_OF: "∈", // U+2208
NOT_ELEMENT_OF: "∉", // U+2209
SUBSET: "⊂", // U+2282
SUPERSET: "⊃", // U+2283
UNION: "", // U+222A
INTERSECTION: "∩", // U+2229
// Arrows and other symbols
RIGHTWARDS_ARROW: "→", // U+2192
LEFTWARDS_ARROW: "←", // U+2190
UP_ARROW: "↑", // U+2191
DOWN_ARROW: "↓", // U+2193
} as const;

View File

@@ -0,0 +1,538 @@
/**
* CSTードビジターのメインモジュール
* 各種ビジターモジュールを統合して使用
*/
import {
DeclarationVisitorMethods,
type DeclarationVisitor,
} from "./visitor/declarations";
import {
ExpressionVisitorMethods,
type ExpressionVisitor,
} from "./visitor/expressions";
import { Scala3VisitorMethods, type Scala3Visitor } from "./visitor/scala3";
import {
StatementVisitorMethods,
type StatementVisitor,
} from "./visitor/statements";
import { TypeVisitorMethods, type TypeVisitor } from "./visitor/types";
import {
getPrintWidth,
getTabWidth,
formatStatement,
formatStringLiteral,
createIndent,
attachOriginalComments,
} from "./visitor/utils";
import type { PrintContext, CSTNode } from "./visitor/utils";
import type { ScalaCstNode } from "@/common/prettier/plugins/scala/scala-parser";
// 外部使用のためのユーティリティ型の再エクスポート
export type { PrintContext, CSTNode, PrettierOptions } from "./visitor/utils";
// 後方互換性のための型エイリアス
type VisitorContext = PrintContext;
/**
* CSTードを訪問してフォーマット済みのテキストに変換するビジター
* 各種言語構造に対応するビジターモジュールを統合
*/
export class CstNodeVisitor
implements
DeclarationVisitor,
ExpressionVisitor,
StatementVisitor,
TypeVisitor,
Scala3Visitor
{
// ビジターモジュールの初期化
private declarations = new DeclarationVisitorMethods(this);
private expressions = new ExpressionVisitorMethods(this);
private statements = new StatementVisitorMethods(this);
private types = new TypeVisitorMethods(this);
private scala3 = new Scala3VisitorMethods(this);
/**
* CSTードを訪問してフォーマット済みテキストに変換
* @param node - 訪問対象のCSTード
* @param ctx - 印刷コンテキスト(オプション、パスなど)
* @returns フォーマット済みの文字列
*/
visit(node: ScalaCstNode, ctx: PrintContext): string {
if (!node) return "";
try {
// オリジナルコメントを含むルートノードの処理
if (
"type" in node &&
node.type === "compilationUnit" &&
"originalComments" in node &&
node.originalComments
) {
const nodeResult = this.visitCore(node, ctx);
// originalCommentsの安全な型変換
const comments = Array.isArray(node.originalComments)
? (node.originalComments as unknown as CSTNode[])
: [];
return attachOriginalComments(nodeResult, comments);
}
return this.visitCore(node, ctx);
} catch (error) {
const nodeName = "name" in node ? node.name : "unknown";
console.error(`Error visiting node ${nodeName}:`, error);
// フォーマットエラー時の安全なフォールバック
if ("image" in node && node.image) {
return String(node.image);
}
return `/* FORMAT ERROR: ${nodeName} */`;
}
}
/**
* CSTード訪問のコアロジック
* @param node - 訪問対象のCSTード
* @param ctx - 印刷コンテキスト
* @returns フォーマット済みの文字列
*/
private visitCore(node: CSTNode, ctx: PrintContext): string {
try {
// トークンノードの処理
if ("image" in node && node.image !== undefined) {
return node.image;
}
// ルール名によるCSTードの処理
if ("name" in node && node.name) {
// ルール名の最初の文字を大文字化
const ruleName = node.name.charAt(0).toUpperCase() + node.name.slice(1);
const methodName = `visit${ruleName}`;
if (
typeof (this as Record<string, unknown>)[methodName] === "function"
) {
return (
(this as Record<string, unknown>)[methodName] as (
node: ScalaCstNode,
ctx: VisitorContext,
) => string
)(node, ctx);
}
}
// If no specific visitor method exists, try default handling by type
if ("children" in node && node.children) {
return this.visitChildren(node, ctx);
}
return "";
} catch (error) {
const nodeName = "name" in node ? node.name : "unknown";
console.error(`Error in visitCore for ${nodeName}:`, error);
// Try to recover by visiting children directly
if ("children" in node && node.children) {
try {
return this.visitChildren(node, ctx);
} catch (childError) {
console.error(`Error visiting children of ${nodeName}:`, childError);
return `/* ERROR: ${nodeName} */`;
}
}
return "image" in node && node.image ? node.image : "";
}
}
visitChildren(node: CSTNode, ctx: PrintContext): string {
const parts: string[] = [];
if (!("children" in node) || !node.children) return "";
try {
for (const [key, children] of Object.entries(node.children)) {
if (Array.isArray(children)) {
for (const child of children) {
try {
// Type guard for ScalaCstNode
if ("children" in child && "name" in child) {
const part = this.visit(child as ScalaCstNode, ctx);
if (part) {
parts.push(part);
}
} else {
// Handle IToken
const tokenImage = "image" in child ? child.image : "";
if (tokenImage) {
parts.push(tokenImage);
}
}
} catch (childError) {
const childName = "name" in child ? child.name : "token";
console.error(
`Error visiting child ${childName || "unknown"} in ${key}:`,
childError,
);
// Continue with next child instead of failing completely
parts.push(`/* ERROR: ${childName || "unknown"} */`);
}
}
}
}
} catch (error) {
console.error(
`Error visiting children of ${node.name || "unknown"}:`,
error,
);
return `/* ERROR: ${node.name || "unknown"} children */`;
}
return parts.join(" ");
}
// Utility methods for shared functionality
getIndentation(ctx: PrintContext): string {
return createIndent(1, ctx);
}
getPrintWidth(ctx: PrintContext): number {
return getPrintWidth(ctx);
}
getTabWidth(ctx: PrintContext): number {
return getTabWidth(ctx);
}
formatStatement(statement: string, ctx: PrintContext): string {
return formatStatement(statement, ctx);
}
formatStringLiteral(content: string, ctx: PrintContext): string {
return formatStringLiteral(content, ctx);
}
// ==========================================
// Delegation methods to modular visitors
// ==========================================
// Compilation unit and top-level structure
visitCompilationUnit(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitCompilationUnit(node, ctx);
}
// Package and imports/exports
visitPackageClause(node: ScalaCstNode, ctx: PrintContext): string {
return this.statements.visitPackageClause(node, ctx);
}
visitImportClause(node: ScalaCstNode, ctx: PrintContext): string {
return this.statements.visitImportClause(node, ctx);
}
visitImportExpression(node: ScalaCstNode, ctx: PrintContext): string {
return this.statements.visitImportExpression(node, ctx);
}
visitImportSelector(node: ScalaCstNode, ctx: PrintContext): string {
return this.statements.visitImportSelector(node, ctx);
}
visitExportClause(node: ScalaCstNode, ctx: PrintContext): string {
return this.scala3.visitExportClause(node, ctx);
}
visitExportExpression(node: ScalaCstNode, ctx: PrintContext): string {
return this.scala3.visitExportExpression(node, ctx);
}
visitExportSelector(node: ScalaCstNode, ctx: PrintContext): string {
return this.scala3.visitExportSelector(node, ctx);
}
// Definitions and declarations
visitTopLevelDefinition(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitTopLevelDefinition(node, ctx);
}
visitDefinition(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitDefinition(node, ctx);
}
visitAnnotations(annotations: CSTNode[], ctx: PrintContext): string {
return this.statements.visitAnnotations(annotations, ctx);
}
visitAnnotation(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitAnnotation(node, ctx);
}
visitAnnotationArgument(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitAnnotationArgument(node, ctx);
}
visitModifiers(modifiers: CSTNode[], ctx: PrintContext): string {
return this.statements.visitModifiers(modifiers, ctx);
}
// Class-related declarations
visitClassDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitClassDefinition(node, ctx);
}
visitObjectDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitObjectDefinition(node, ctx);
}
visitTraitDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitTraitDefinition(node, ctx);
}
visitValDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitValDefinition(node, ctx);
}
visitVarDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitVarDefinition(node, ctx);
}
visitDefDefinition(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitDefDefinition(node, ctx);
}
visitTypeDefinition(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitTypeDefinition(node, ctx);
}
visitAuxiliaryConstructor(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitAuxiliaryConstructor(node, ctx);
}
visitClassParameters(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitClassParameters(node, ctx);
}
visitClassParameter(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitClassParameter(node, ctx);
}
visitParameterLists(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitParameterLists(node, ctx);
}
visitParameterList(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitParameterList(node, ctx);
}
visitParameter(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitParameter(node, ctx);
}
visitTypeParameters(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitTypeParameters(node, ctx);
}
visitTypeParameter(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitTypeParameter(node, ctx);
}
visitExtendsClause(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitExtendsClause(node, ctx);
}
visitClassBody(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitClassBody(node, ctx);
}
visitClassMember(node: CSTNode, ctx: PrintContext): string {
return this.declarations.visitClassMember(node, ctx);
}
// Type system
visitType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitType(node, ctx);
}
visitMatchType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitMatchType(node, ctx);
}
visitMatchTypeCase(node: CSTNode, ctx: PrintContext): string {
return this.types.visitMatchTypeCase(node, ctx);
}
visitUnionType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitUnionType(node, ctx);
}
visitIntersectionType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitIntersectionType(node, ctx);
}
visitBaseType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitBaseType(node, ctx);
}
visitTupleTypeOrParenthesized(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTupleTypeOrParenthesized(node, ctx);
}
visitSimpleType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitSimpleType(node, ctx);
}
visitTypeArgument(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeArgument(node, ctx);
}
visitTypeArgumentUnion(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeArgumentUnion(node, ctx);
}
visitTypeArgumentIntersection(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeArgumentIntersection(node, ctx);
}
visitTypeArgumentSimple(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeArgumentSimple(node, ctx);
}
visitTypeLambda(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeLambda(node, ctx);
}
visitTypeLambdaParameter(node: CSTNode, ctx: PrintContext): string {
return this.types.visitTypeLambdaParameter(node, ctx);
}
visitDependentFunctionType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitDependentFunctionType(node, ctx);
}
visitDependentParameter(node: CSTNode, ctx: PrintContext): string {
return this.types.visitDependentParameter(node, ctx);
}
// Expressions
visitExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitExpression(node, ctx);
}
visitPostfixExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitPostfixExpression(node, ctx);
}
visitPrimaryExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitPrimaryExpression(node, ctx);
}
visitAssignmentStatement(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitAssignmentStatement(node, ctx);
}
visitAssignmentOrInfixExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitAssignmentOrInfixExpression(node, ctx);
}
visitInfixOperator(node: ScalaCstNode, ctx: PrintContext): string {
return this.expressions.visitInfixOperator(node, ctx);
}
visitLiteral(node: ScalaCstNode, ctx: PrintContext): string {
return this.expressions.visitLiteral(node, ctx);
}
visitQualifiedIdentifier(node: ScalaCstNode, ctx: PrintContext): string {
return this.expressions.visitQualifiedIdentifier(node, ctx);
}
visitNewExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitNewExpression(node, ctx);
}
visitIfExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitIfExpression(node, ctx);
}
visitWhileExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitWhileExpression(node, ctx);
}
visitTryExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitTryExpression(node, ctx);
}
visitForExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitForExpression(node, ctx);
}
visitGenerator(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitGenerator(node, ctx);
}
visitCaseClause(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitCaseClause(node, ctx);
}
visitBlockExpression(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitBlockExpression(node, ctx);
}
visitPartialFunctionLiteral(node: CSTNode, ctx: PrintContext): string {
return this.expressions.visitPartialFunctionLiteral(node, ctx);
}
// Statements
visitBlockStatement(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitBlockStatement(node, ctx);
}
visitPattern(node: CSTNode, ctx: PrintContext): string {
return this.statements.visitPattern(node, ctx);
}
// Scala 3 specific features
visitEnumDefinition(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitEnumDefinition(node, ctx);
}
visitEnumCase(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitEnumCase(node, ctx);
}
visitExtensionDefinition(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitExtensionDefinition(node, ctx);
}
visitExtensionMember(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitExtensionMember(node, ctx);
}
visitGivenDefinition(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitGivenDefinition(node, ctx);
}
visitQuoteExpression(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitQuoteExpression(node, ctx);
}
visitSpliceExpression(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitSpliceExpression(node, ctx);
}
visitPolymorphicFunctionLiteral(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitPolymorphicFunctionLiteral(node, ctx);
}
visitPolymorphicFunctionType(node: CSTNode, ctx: PrintContext): string {
return this.types.visitPolymorphicFunctionType(node, ctx);
}
visitPolymorphicTypeParameter(node: CSTNode, ctx: PrintContext): string {
return this.types.visitPolymorphicTypeParameter(node, ctx);
}
visitContextFunctionType(node: CSTNode, ctx: PrintContext): string {
return this.scala3.visitContextFunctionType(node, ctx);
}
}

View File

@@ -0,0 +1,640 @@
/**
* Declaration visitor methods for class, object, trait, method, and other definitions
*/
import {
formatStatement,
getPrintWidth,
getChildNodes,
getFirstChild,
createIndent,
getNodeImage,
} from "./utils";
import type { PrintContext, CSTNode } from "./utils";
export interface DeclarationVisitor {
visit(node: CSTNode, ctx: PrintContext): string;
visitModifiers(modifiers: CSTNode[], ctx: PrintContext): string;
getIndentation(ctx: PrintContext): string;
}
export class DeclarationVisitorMethods {
private visitor: DeclarationVisitor;
constructor(visitor: DeclarationVisitor) {
this.visitor = visitor;
}
visitClassDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Add class keyword (don't duplicate if already handled by modifiers)
const classToken = getFirstChild(node, "Class");
if (classToken) {
result += getNodeImage(classToken) + " ";
}
// Add class name
const identifierToken = getFirstChild(node, "Identifier");
if (identifierToken) {
result += getNodeImage(identifierToken);
}
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
// Add constructor annotations
const annotations = getChildNodes(node, "annotation");
if (annotations.length > 0) {
result +=
" " +
annotations
.map((ann: CSTNode) => this.visitor.visit(ann, ctx))
.join(" ");
}
const classParameters = getFirstChild(node, "classParameters");
if (classParameters) {
result += this.visitor.visit(classParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
const classBody = getFirstChild(node, "classBody");
if (classBody) {
result += " " + this.visitor.visit(classBody, ctx);
}
return result;
}
visitObjectDefinition(node: CSTNode, ctx: PrintContext): string {
const identifierToken = getFirstChild(node, "Identifier");
let result =
"object " + (identifierToken ? getNodeImage(identifierToken) : "");
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
const classBody = getFirstChild(node, "classBody");
if (classBody) {
result += " " + this.visitor.visit(classBody, ctx);
}
return result;
}
visitTraitDefinition(node: CSTNode, ctx: PrintContext): string {
const identifier = getFirstChild(node, "Identifier");
let result = "trait " + (identifier ? getNodeImage(identifier) : "");
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
const traitBody = getFirstChild(node, "classBody");
if (traitBody) {
result += " " + this.visitor.visit(traitBody, ctx);
}
return result;
}
visitEnumDefinition(node: CSTNode, ctx: PrintContext): string {
const identifierToken = getFirstChild(node, "Identifier");
let result =
"enum " + (identifierToken ? getNodeImage(identifierToken) : "");
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const classParameters = getFirstChild(node, "classParameters");
if (classParameters) {
result += this.visitor.visit(classParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
result += " {\n";
const enumCases = getChildNodes(node, "enumCase");
if (enumCases.length > 0) {
const indent = this.visitor.getIndentation(ctx);
const cases = enumCases.map(
(c: CSTNode) => indent + this.visitor.visit(c, ctx),
);
result += cases.join("\n");
}
result += "\n}";
return result;
}
visitEnumCase(node: CSTNode, ctx: PrintContext): string {
const identifierToken = getFirstChild(node, "Identifier");
let result =
"case " + (identifierToken ? getNodeImage(identifierToken) : "");
const classParameters = getFirstChild(node, "classParameters");
if (classParameters) {
result += this.visitor.visit(classParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
return result;
}
visitExtensionDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "extension";
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const identifierToken = getFirstChild(node, "Identifier");
const typeNode = getFirstChild(node, "type");
result +=
" (" + (identifierToken ? getNodeImage(identifierToken) : "") + ": ";
if (typeNode) {
result += this.visitor.visit(typeNode, ctx);
}
result += ") {\n";
const extensionMembers = getChildNodes(node, "extensionMember");
if (extensionMembers.length > 0) {
const members = extensionMembers.map(
(m: CSTNode) => " " + this.visitor.visit(m, ctx),
);
result += members.join("\n");
}
result += "\n}";
return result;
}
visitExtensionMember(node: CSTNode, ctx: PrintContext): string {
const modifierNodes = getChildNodes(node, "modifier");
const modifiers = this.visitor.visitModifiers(modifierNodes, ctx);
const defDefinition = getFirstChild(node, "defDefinition");
const definition = defDefinition
? this.visitor.visit(defDefinition, ctx)
: "";
return modifiers ? modifiers + " " + definition : definition;
}
visitValDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "val ";
// Handle pattern or identifier
const pattern = getFirstChild(node, "pattern");
const identifierToken = getFirstChild(node, "Identifier");
if (pattern) {
result += this.visitor.visit(pattern, ctx);
} else if (identifierToken) {
result += getNodeImage(identifierToken);
}
const colonToken = getFirstChild(node, "Colon");
if (colonToken) {
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
}
const equalsToken = getFirstChild(node, "Equals");
if (equalsToken) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
}
return formatStatement(result, ctx);
}
visitVarDefinition(node: CSTNode, ctx: PrintContext): string {
const identifierToken = getFirstChild(node, "Identifier");
let result =
"var " + (identifierToken ? getNodeImage(identifierToken) : "");
const colonToken = getFirstChild(node, "Colon");
if (colonToken) {
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
}
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
return formatStatement(result, ctx);
}
visitDefDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "def ";
const identifierToken = getFirstChild(node, "Identifier");
const thisToken = getFirstChild(node, "This");
if (identifierToken) {
result += getNodeImage(identifierToken);
} else if (thisToken) {
result += "this";
}
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const parameterLists = getFirstChild(node, "parameterLists");
if (parameterLists) {
result += this.visitor.visit(parameterLists, ctx);
}
const colonToken = getFirstChild(node, "Colon");
if (colonToken) {
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
}
const equalsToken = getFirstChild(node, "Equals");
if (equalsToken) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
return formatStatement(result, ctx);
}
return result;
}
visitGivenDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "given";
const identifierToken = getFirstChild(node, "Identifier");
if (identifierToken) {
// Named given with potential parameters: given name[T](using ord: Type): Type
result += " " + getNodeImage(identifierToken);
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const parameterLists = getFirstChild(node, "parameterLists");
if (parameterLists) {
result += this.visitor.visit(parameterLists, ctx);
}
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
} else {
// Anonymous given: given Type = expression
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += " " + this.visitor.visit(typeNode, ctx);
}
}
const equalsToken = getFirstChild(node, "Equals");
if (equalsToken) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
}
return result;
}
visitTypeDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle opaque types
const opaqueToken = getFirstChild(node, "Opaque");
if (opaqueToken) {
result += "opaque ";
}
const identifierToken = getFirstChild(node, "Identifier");
result += "type " + (identifierToken ? getNodeImage(identifierToken) : "");
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += " = " + this.visitor.visit(typeNode, ctx);
}
return result;
}
visitAuxiliaryConstructor(node: CSTNode, ctx: PrintContext): string {
let result = "def this";
// CST uses "parameterList" (singular) for auxiliary constructors
const parameterList = getFirstChild(node, "parameterList");
if (parameterList) {
result += this.visitor.visit(parameterList, ctx);
}
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
return result;
}
visitClassParameters(node: CSTNode, ctx: PrintContext): string {
const params = getChildNodes(node, "classParameter");
if (params.length === 0) {
return "()";
}
const paramStrings = params.map((p: CSTNode) => this.visitor.visit(p, ctx));
const printWidth = getPrintWidth(ctx);
// Check if single line is appropriate
const singleLine = `(${paramStrings.join(", ")})`;
// Use single line if it fits within printWidth and is reasonably short
if (
params.length === 1 &&
singleLine.length <= Math.min(printWidth * 0.6, 40)
) {
return singleLine;
}
// Use multi-line format for multiple parameters or long single parameter
const indent = this.visitor.getIndentation(ctx);
// Format each parameter on its own line without trailing comma for class parameters
const indentedParams = paramStrings.map((param: string) => indent + param);
return `(\n${indentedParams.join(",\n")}\n)`;
}
visitClassParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
const modifierNodes = getChildNodes(node, "modifier");
if (modifierNodes.length > 0) {
const modifiers = this.visitor.visitModifiers(modifierNodes, ctx);
result += modifiers + " ";
}
const valToken = getFirstChild(node, "Val");
const varToken = getFirstChild(node, "Var");
if (valToken) {
result += "val ";
} else if (varToken) {
result += "var ";
}
const identifierToken = getFirstChild(node, "Identifier");
if (identifierToken) {
result += getNodeImage(identifierToken);
}
result += ": ";
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += this.visitor.visit(typeNode, ctx);
}
const equalsToken = getFirstChild(node, "Equals");
if (equalsToken) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
}
return result;
}
visitParameterLists(node: CSTNode, ctx: PrintContext): string {
const parameterLists = getChildNodes(node, "parameterList");
return parameterLists
.map((list: CSTNode) => this.visitor.visit(list, ctx))
.join("");
}
visitParameterList(node: CSTNode, ctx: PrintContext): string {
const params = getChildNodes(node, "parameter");
if (params.length === 0) {
return "()";
}
const paramStrings = params.map((p: CSTNode) => this.visitor.visit(p, ctx));
return "(" + paramStrings.join(", ") + ")";
}
visitParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
const usingToken = getFirstChild(node, "Using");
const implicitToken = getFirstChild(node, "Implicit");
if (usingToken) {
result += "using ";
} else if (implicitToken) {
result += "implicit ";
}
const identifierToken = getFirstChild(node, "Identifier");
if (identifierToken) {
result += getNodeImage(identifierToken);
}
result += ": ";
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += this.visitor.visit(typeNode, ctx);
}
const equalsToken = getFirstChild(node, "Equals");
if (equalsToken) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
}
return result;
}
visitTypeParameters(node: CSTNode, ctx: PrintContext): string {
const params = getChildNodes(node, "typeParameter");
if (params.length === 0) {
return "";
}
const paramStrings = params.map((p: CSTNode) => this.visitor.visit(p, ctx));
return "[" + paramStrings.join(", ") + "]";
}
visitTypeParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle variance annotations
const plusToken = getFirstChild(node, "Plus");
const minusToken = getFirstChild(node, "Minus");
if (plusToken) {
result += "+";
} else if (minusToken) {
result += "-";
}
const identifierToken = getFirstChild(node, "Identifier");
if (identifierToken) {
result += getNodeImage(identifierToken);
}
// Add bounds
const subtypeOfToken = getFirstChild(node, "SubtypeOf");
const supertypeOfToken = getFirstChild(node, "SupertypeOf");
const typeNodes = getChildNodes(node, "type");
if (subtypeOfToken && typeNodes.length > 0) {
result += " <: " + this.visitor.visit(typeNodes[0], ctx);
}
if (supertypeOfToken && typeNodes.length > 1) {
result += " >: " + this.visitor.visit(typeNodes[1], ctx);
} else if (supertypeOfToken && typeNodes.length === 1 && !subtypeOfToken) {
result += " >: " + this.visitor.visit(typeNodes[0], ctx);
}
return result;
}
visitExtendsClause(node: CSTNode, ctx: PrintContext): string {
const typeNodes = getChildNodes(node, "type");
if (typeNodes.length === 0) {
return "";
}
let result = "extends " + this.visitor.visit(typeNodes[0], ctx);
const withToken = getFirstChild(node, "With");
if (withToken && typeNodes.length > 1) {
const withTypes = typeNodes
.slice(1)
.map((t: CSTNode) => this.visitor.visit(t, ctx));
result += " with " + withTypes.join(" with ");
}
return result;
}
visitClassBody(node: CSTNode, ctx: PrintContext): string {
const classMembers = getChildNodes(node, "classMember");
if (classMembers.length === 0) {
return "{}";
}
// Increase indent level for class members
const nestedCtx = {
...ctx,
indentLevel: ctx.indentLevel + 1,
};
const members = classMembers.map((m: CSTNode) =>
this.visitor.visit(m, nestedCtx),
);
const indent = createIndent(1, ctx);
return "{\n" + members.map((m: string) => indent + m).join("\n") + "\n}";
}
visitClassMember(node: CSTNode, ctx: PrintContext): string {
// Handle different types of class members
const defDefinition = getFirstChild(node, "defDefinition");
if (defDefinition) {
return this.visitor.visit(defDefinition, ctx);
}
const auxiliaryConstructor = getFirstChild(node, "auxiliaryConstructor");
if (auxiliaryConstructor) {
return this.visitor.visit(auxiliaryConstructor, ctx);
}
const valDefinition = getFirstChild(node, "valDefinition");
if (valDefinition) {
return this.visitor.visit(valDefinition, ctx);
}
const varDefinition = getFirstChild(node, "varDefinition");
if (varDefinition) {
return this.visitor.visit(varDefinition, ctx);
}
const classDefinition = getFirstChild(node, "classDefinition");
if (classDefinition) {
return this.visitor.visit(classDefinition, ctx);
}
const objectDefinition = getFirstChild(node, "objectDefinition");
if (objectDefinition) {
return this.visitor.visit(objectDefinition, ctx);
}
const traitDefinition = getFirstChild(node, "traitDefinition");
if (traitDefinition) {
return this.visitor.visit(traitDefinition, ctx);
}
const typeDefinition = getFirstChild(node, "typeDefinition");
if (typeDefinition) {
return this.visitor.visit(typeDefinition, ctx);
}
const definition = getFirstChild(node, "definition");
if (definition) {
return this.visitor.visit(definition, ctx);
}
return "";
}
}

View File

@@ -0,0 +1,836 @@
/**
* Expression visitor methods for handling various expression types
*/
import {
formatStringLiteral,
getChildNodes,
getFirstChild,
createIndent,
getNodeImage,
} from "./utils";
import type { PrintContext, CSTNode } from "./utils";
export interface ExpressionVisitor {
visit(node: CSTNode, ctx: PrintContext): string;
}
export class ExpressionVisitorMethods {
private visitor: ExpressionVisitor;
constructor(visitor: ExpressionVisitor) {
this.visitor = visitor;
}
visitExpression(node: CSTNode, ctx: PrintContext): string {
// Handle PartialFunction literals: { case ... }
const partialFunctionLiteral = getFirstChild(
node,
"partialFunctionLiteral",
);
if (partialFunctionLiteral) {
return this.visitor.visit(partialFunctionLiteral, ctx);
}
// Handle lambda expressions with parameter list: (x: Int, y: Int) => x + y
const parameterList = getFirstChild(node, "parameterList");
const arrow = getChildNodes(node, "Arrow");
if (parameterList && arrow.length > 0) {
const expression = getFirstChild(node, "expression");
return (
this.visitor.visit(parameterList, ctx) +
" => " +
(expression ? this.visitor.visit(expression, ctx) : "")
);
}
// Handle block lambda expressions: { x => ... }
const leftBrace = getChildNodes(node, "LeftBrace");
const identifier = getChildNodes(node, "Identifier");
const arrowNodes = getChildNodes(node, "Arrow");
if (
leftBrace.length > 0 &&
identifier.length > 0 &&
arrowNodes.length > 0
) {
let result = "{ " + getNodeImage(identifier[0]) + " =>";
const statements: string[] = [];
// Create nested context for lambda body
const nestedCtx = {
...ctx,
indentLevel: ctx.indentLevel + 1,
};
// Add statements (val/var/def definitions)
const blockStatements = getChildNodes(node, "blockStatement");
if (blockStatements.length > 0) {
statements.push(
...blockStatements.map((stmt: CSTNode) =>
this.visitor.visit(stmt, nestedCtx),
),
);
}
// Add final expression
const finalExpression = getFirstChild(node, "expression");
if (finalExpression) {
statements.push(this.visitor.visit(finalExpression, nestedCtx));
}
if (statements.length === 0) {
result += " }";
} else if (statements.length === 1) {
// Single expression - keep on same line if short
const stmt = statements[0];
if (stmt.length < 50) {
result += " " + stmt + " }";
} else {
const indent = createIndent(1, ctx);
result += "\n" + indent + stmt + "\n}";
}
} else {
// Multiple statements - use multiple lines
const indent = createIndent(1, ctx);
const indentedStmts = statements.map((stmt) => indent + stmt);
result += "\n" + indentedStmts.join("\n") + "\n}";
}
return result;
}
// Handle polymorphic function literal: [T] => (x: T) => x
const polymorphicFunctionLiteral = getFirstChild(
node,
"polymorphicFunctionLiteral",
);
if (polymorphicFunctionLiteral) {
return this.visitor.visit(polymorphicFunctionLiteral, ctx);
}
// Handle simple lambda expressions: x => x * 2
const simpleIdentifier = getChildNodes(node, "Identifier");
const simpleArrow = getChildNodes(node, "Arrow");
if (simpleIdentifier.length > 0 && simpleArrow.length > 0) {
const expression = getFirstChild(node, "expression");
return (
getNodeImage(simpleIdentifier[0]) +
" => " +
(expression ? this.visitor.visit(expression, ctx) : "")
);
}
// Handle assignmentOrInfixExpression
const assignmentOrInfixExpression = getFirstChild(
node,
"assignmentOrInfixExpression",
);
if (assignmentOrInfixExpression) {
return this.visitor.visit(assignmentOrInfixExpression, ctx);
}
// Handle regular expressions (fallback for older structure)
const postfixExpressions = getChildNodes(node, "postfixExpression");
if (postfixExpressions.length > 0) {
let result = this.visitor.visit(postfixExpressions[0], ctx);
const infixOperators = getChildNodes(node, "infixOperator");
if (infixOperators.length > 0) {
for (let i = 0; i < infixOperators.length; i++) {
result +=
" " +
this.visitor.visit(infixOperators[i], ctx) +
" " +
(postfixExpressions[i + 1]
? this.visitor.visit(postfixExpressions[i + 1], ctx)
: "");
}
}
return result;
}
return "";
}
visitPostfixExpression(node: CSTNode, ctx: PrintContext): string {
const primaryExpression = getFirstChild(node, "primaryExpression");
let result = primaryExpression
? this.visitor.visit(primaryExpression, ctx)
: "";
// Handle method calls and member access
const dots = getChildNodes(node, "Dot");
if (dots.length > 0) {
const identifiers = getChildNodes(node, "Identifier");
for (let i = 0; i < dots.length; i++) {
result += ".";
// Handle member access or method call
// Identifiers after the first one correspond to members after dots
if (identifiers.length > i) {
result += getNodeImage(identifiers[i]);
}
// Add arguments if this is a method call
const leftParens = getChildNodes(node, "LeftParen");
if (leftParens.length > i) {
result += "(";
// Find expressions for this argument list
const startIdx = i * 10; // Rough heuristic for argument grouping
const expressions = getChildNodes(node, "expression");
const relevantExpressions = expressions.slice(
startIdx,
startIdx + 10,
);
if (relevantExpressions.length > 0) {
const args = relevantExpressions.map((e: CSTNode) =>
this.visitor.visit(e, ctx),
);
result += args.join(", ");
}
result += ")";
}
}
}
// Handle type arguments
const leftBrackets = getChildNodes(node, "LeftBracket");
if (leftBrackets.length > 0) {
result += "[";
const types = getChildNodes(node, "type");
if (types.length > 0) {
const typeStrings = types.map((t: CSTNode) =>
this.visitor.visit(t, ctx),
);
result += typeStrings.join(", ");
}
result += "]";
}
// Handle match expressions
const matchTokens = getChildNodes(node, "Match");
if (matchTokens.length > 0) {
result += " match {\n";
const caseClauses = getChildNodes(node, "caseClause");
if (caseClauses.length > 0) {
const cases = caseClauses.map(
(c: CSTNode) => " " + this.visitor.visit(c, ctx),
);
result += cases.join("\n");
result += "\n";
}
result += "}";
}
// Handle method application without dot
const methodLeftParens = getChildNodes(node, "LeftParen");
const methodDots = getChildNodes(node, "Dot");
if (methodLeftParens.length > 0 && methodDots.length === 0) {
result += "(";
const methodExpressions = getChildNodes(node, "expression");
if (methodExpressions.length > 0) {
const args = methodExpressions.map((e: CSTNode) =>
this.visitor.visit(e, ctx),
);
result += args.join(", ");
}
result += ")";
}
// Handle block lambda expressions: method { param => ... }
const leftBrace = getChildNodes(node, "LeftBrace");
const arrowNodes = getChildNodes(node, "Arrow");
const identifiers = getChildNodes(node, "Identifier");
if (
leftBrace.length > 0 &&
arrowNodes.length > 0 &&
identifiers.length > 1
) {
// The lambda parameter is the second identifier (first is method name)
const lambdaParam = getNodeImage(identifiers[1]);
result += " { " + lambdaParam + " =>";
// Create nested context for lambda body
const nestedCtx = {
...ctx,
indentLevel: ctx.indentLevel + 1,
};
// Process block statements
const blockStatements = getChildNodes(node, "blockStatement");
const statements: string[] = [];
for (const stmt of blockStatements) {
statements.push(this.visitor.visit(stmt, nestedCtx));
}
if (statements.length === 0) {
result += " }";
} else if (statements.length === 1) {
// Single statement - keep on same line if short
const stmt = statements[0];
if (stmt.length < 50) {
result += " " + stmt + " }";
} else {
const indent = createIndent(1, ctx);
result += "\n" + indent + stmt + "\n}";
}
} else {
// Multiple statements - use multiple lines
const indent = createIndent(1, ctx);
const indentedStmts = statements.map((stmt) => indent + stmt);
result += "\n" + indentedStmts.join("\n") + "\n}";
}
}
return result;
}
visitPrimaryExpression(node: CSTNode, ctx: PrintContext): string {
const literal = getFirstChild(node, "literal");
if (literal) {
return this.visitor.visit(literal, ctx);
}
const identifier = getFirstChild(node, "Identifier");
if (identifier) {
return getNodeImage(identifier);
}
const thisToken = getChildNodes(node, "This");
if (thisToken.length > 0) {
return "this";
}
const partialFunctionLiteral = getFirstChild(
node,
"partialFunctionLiteral",
);
if (partialFunctionLiteral) {
return this.visitor.visit(partialFunctionLiteral, ctx);
}
const newExpression = getFirstChild(node, "newExpression");
if (newExpression) {
return this.visitor.visit(newExpression, ctx);
}
const forExpression = getFirstChild(node, "forExpression");
if (forExpression) {
return this.visitor.visit(forExpression, ctx);
}
const ifExpression = getFirstChild(node, "ifExpression");
if (ifExpression) {
return this.visitor.visit(ifExpression, ctx);
}
const whileExpression = getFirstChild(node, "whileExpression");
if (whileExpression) {
return this.visitor.visit(whileExpression, ctx);
}
const tryExpression = getFirstChild(node, "tryExpression");
if (tryExpression) {
return this.visitor.visit(tryExpression, ctx);
}
const exclamation = getChildNodes(node, "Exclamation");
if (exclamation.length > 0) {
// Handle negation operator
const postfixExpression = getFirstChild(node, "postfixExpression");
if (postfixExpression) {
const result = this.visitor.visit(postfixExpression, ctx);
return "!" + result;
}
return "!";
}
const bitwiseTilde = getChildNodes(node, "BitwiseTilde");
if (bitwiseTilde.length > 0) {
// Handle bitwise complement operator
const postfixExpression = getFirstChild(node, "postfixExpression");
return (
"~" +
(postfixExpression ? this.visitor.visit(postfixExpression, ctx) : "")
);
}
const leftParen = getChildNodes(node, "LeftParen");
if (leftParen.length > 0) {
const expression = getFirstChild(node, "expression");
const assignmentOrInfixExpression = getFirstChild(
node,
"assignmentOrInfixExpression",
);
// Try expression first, then assignmentOrInfixExpression
const content = expression
? this.visitor.visit(expression, ctx)
: assignmentOrInfixExpression
? this.visitor.visit(assignmentOrInfixExpression, ctx)
: "";
return "(" + content + ")";
}
const blockExpression = getFirstChild(node, "blockExpression");
if (blockExpression) {
return this.visitor.visit(blockExpression, ctx);
}
const quoteExpression = getFirstChild(node, "quoteExpression");
if (quoteExpression) {
return this.visitor.visit(quoteExpression, ctx);
}
const spliceExpression = getFirstChild(node, "spliceExpression");
if (spliceExpression) {
return this.visitor.visit(spliceExpression, ctx);
}
return "";
}
visitAssignmentOrInfixExpression(node: CSTNode, ctx: PrintContext): string {
const postfixExpressions = getChildNodes(node, "postfixExpression");
let result =
postfixExpressions.length > 0
? this.visitor.visit(postfixExpressions[0], ctx)
: "";
// Handle assignment operators (including named arguments)
const equals = getChildNodes(node, "Equals");
const plusEquals = getChildNodes(node, "PlusEquals");
const minusEquals = getChildNodes(node, "MinusEquals");
const starEquals = getChildNodes(node, "StarEquals");
const slashEquals = getChildNodes(node, "SlashEquals");
const percentEquals = getChildNodes(node, "PercentEquals");
const sbtAssign = getChildNodes(node, "SbtAssign");
const operator =
equals[0] ||
plusEquals[0] ||
minusEquals[0] ||
starEquals[0] ||
slashEquals[0] ||
percentEquals[0] ||
sbtAssign[0];
if (operator) {
result += " " + getNodeImage(operator) + " ";
const expressions = getChildNodes(node, "expression");
if (expressions.length > 0) {
result += this.visitor.visit(expressions[0], ctx);
}
}
// Handle infix operators
const infixOperators = getChildNodes(node, "infixOperator");
if (infixOperators.length > 0) {
for (let i = 0; i < infixOperators.length; i++) {
result += " " + this.visitor.visit(infixOperators[i], ctx) + " ";
if (postfixExpressions.length > i + 1) {
result += this.visitor.visit(postfixExpressions[i + 1], ctx);
}
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
visitInfixOperator(node: CSTNode, _ctx: PrintContext): string {
// Handle all possible infix operators
const operators = [
"Plus",
"Minus",
"Star",
"Slash",
"Percent",
"DoubleStar",
"LeftShift",
"RightShift",
"UnsignedRightShift",
"BitwiseAnd",
"BitwiseOr",
"BitwiseXor",
"EqualsEquals",
"NotEquals",
"LessThan",
"LessThanOrEqual",
"GreaterThan",
"GreaterThanOrEqual",
"LogicalAnd",
"LogicalOr",
"DoublePercent",
"Ask",
"To",
"Until",
"PrependOp",
"AppendOp",
"ConcatOp",
"RightArrow",
];
for (const op of operators) {
const tokens = getChildNodes(node, op);
if (tokens.length > 0) {
return getNodeImage(tokens[0]);
}
}
// Fallback to identifier for custom operators
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
return getNodeImage(identifiers[0]);
}
return "";
}
visitLiteral(node: CSTNode, ctx: PrintContext): string {
// Handle all possible literal types
const literalTypes = [
"StringLiteral",
"InterpolatedStringLiteral",
"IntegerLiteral",
"NumberLiteral",
"FloatLiteral",
"BooleanLiteral",
"True",
"False",
"CharLiteral",
"NullLiteral",
"Null",
"ScientificNumber",
];
for (const literalType of literalTypes) {
const tokens = getChildNodes(node, literalType);
if (tokens.length > 0) {
const tokenImage = getNodeImage(tokens[0]);
// Apply singleQuote formatting to string literals
if (tokenImage.startsWith('"') || tokenImage.startsWith("'")) {
return formatStringLiteral(tokenImage, ctx);
}
return tokenImage;
}
}
return "";
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
visitQualifiedIdentifier(node: CSTNode, _ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length === 0) {
return "";
}
let result = getNodeImage(identifiers[0]);
const dots = getChildNodes(node, "Dot");
if (dots.length > 0) {
// Handle mixed identifiers and type keywords
const types = getChildNodes(node, "Type");
for (let i = 0; i < dots.length; i++) {
result += ".";
// Determine which token comes next (identifier or type keyword)
if (i + 1 < identifiers.length) {
result += getNodeImage(identifiers[i + 1]);
} else if (types.length > 0) {
// Use the type keyword (e.g., "type" for .type syntax)
result += getNodeImage(types[0]);
}
}
}
return result;
}
visitNewExpression(node: CSTNode, ctx: PrintContext): string {
const typeNode = getFirstChild(node, "type");
let result = "new " + (typeNode ? this.visitor.visit(typeNode, ctx) : "");
const leftParens = getChildNodes(node, "LeftParen");
if (leftParens.length > 0) {
result += "(";
const expressions = getChildNodes(node, "expression");
if (expressions.length > 0) {
const args = expressions.map((e: CSTNode) =>
this.visitor.visit(e, ctx),
);
result += args.join(", ");
}
result += ")";
}
return result;
}
visitIfExpression(node: CSTNode, ctx: PrintContext): string {
const expressions = getChildNodes(node, "expression");
if (expressions.length < 2) {
return "if";
}
let result = "if (";
result += this.visitor.visit(expressions[0], ctx);
result += ") ";
result += this.visitor.visit(expressions[1], ctx);
const elseTokens = getChildNodes(node, "Else");
if (elseTokens.length > 0 && expressions.length > 2) {
result += " else ";
result += this.visitor.visit(expressions[2], ctx);
}
return result;
}
visitWhileExpression(node: CSTNode, ctx: PrintContext): string {
const expressions = getChildNodes(node, "expression");
if (expressions.length < 2) {
return "while";
}
let result = "while (";
result += this.visitor.visit(expressions[0], ctx);
result += ") ";
result += this.visitor.visit(expressions[1], ctx);
return result;
}
visitTryExpression(node: CSTNode, ctx: PrintContext): string {
const expressions = getChildNodes(node, "expression");
if (expressions.length === 0) {
return "try";
}
let result = "try ";
result += this.visitor.visit(expressions[0], ctx);
const catchTokens = getChildNodes(node, "Catch");
if (catchTokens.length > 0) {
result += " catch {\n";
const caseClauses = getChildNodes(node, "caseClause");
if (caseClauses.length > 0) {
const cases = caseClauses.map(
(c: CSTNode) => " " + this.visitor.visit(c, ctx),
);
result += cases.join("\n");
}
result += "\n}";
}
const finallyTokens = getChildNodes(node, "Finally");
if (finallyTokens.length > 0) {
result += " finally ";
// If there's a catch block, expression[1] is the finally expression
// Otherwise, expression[1] would be the finally expression (no catch)
const finallyExprIndex = catchTokens.length > 0 ? 1 : 1;
if (expressions.length > finallyExprIndex) {
result += this.visitor.visit(expressions[finallyExprIndex], ctx);
}
}
return result;
}
visitForExpression(node: CSTNode, ctx: PrintContext): string {
let result = "for ";
const leftParens = getChildNodes(node, "LeftParen");
const leftBraces = getChildNodes(node, "LeftBrace");
const generators = getChildNodes(node, "generator");
if (leftParens.length > 0) {
result += "(";
if (generators.length > 0) {
const gens = generators.map((g: CSTNode) => this.visitor.visit(g, ctx));
result += gens.join("; ");
}
result += ")";
} else if (leftBraces.length > 0) {
result += "{\n";
if (generators.length > 0) {
const gens = generators.map(
(g: CSTNode) => " " + this.visitor.visit(g, ctx),
);
result += gens.join("\n");
}
result += "\n}";
}
const yieldTokens = getChildNodes(node, "Yield");
if (yieldTokens.length > 0) {
result += " yield ";
} else {
result += " ";
}
const expressions = getChildNodes(node, "expression");
if (expressions.length > 0) {
result += this.visitor.visit(expressions[0], ctx);
}
return result;
}
visitGenerator(node: CSTNode, ctx: PrintContext): string {
const patterns = getChildNodes(node, "pattern");
const expressions = getChildNodes(node, "expression");
if (patterns.length === 0 || expressions.length === 0) {
return "";
}
let result = this.visitor.visit(patterns[0], ctx);
result += " <- " + this.visitor.visit(expressions[0], ctx);
const ifTokens = getChildNodes(node, "If");
if (ifTokens.length > 0) {
for (let i = 0; i < ifTokens.length; i++) {
if (expressions.length > i + 1) {
result += " if " + this.visitor.visit(expressions[i + 1], ctx);
}
}
}
return result;
}
visitCaseClause(node: CSTNode, ctx: PrintContext): string {
const patterns = getChildNodes(node, "pattern");
const expressions = getChildNodes(node, "expression");
if (patterns.length === 0) {
return "case";
}
let result = "case " + this.visitor.visit(patterns[0], ctx);
const ifTokens = getChildNodes(node, "If");
if (ifTokens.length > 0 && expressions.length > 0) {
result += " if " + this.visitor.visit(expressions[0], ctx);
}
const expressionIndex = ifTokens.length > 0 ? 1 : 0;
if (expressions.length > expressionIndex) {
result += " => " + this.visitor.visit(expressions[expressionIndex], ctx);
}
return result;
}
visitBlockExpression(node: CSTNode, ctx: PrintContext): string {
const blockStatements = getChildNodes(node, "blockStatement");
const expressions = getChildNodes(node, "expression");
if (blockStatements.length === 0 && expressions.length === 0) {
return "{}";
}
let result = "{\n";
const statements: string[] = [];
// Create nested context for block contents
const nestedCtx = {
...ctx,
indentLevel: ctx.indentLevel + 1,
};
if (blockStatements.length > 0) {
statements.push(
...blockStatements.map((stmt: CSTNode) =>
this.visitor.visit(stmt, nestedCtx),
),
);
}
if (expressions.length > 0) {
statements.push(this.visitor.visit(expressions[0], nestedCtx));
}
const indent = createIndent(1, ctx);
result += statements.map((stmt) => indent + stmt).join("\n");
result += "\n}";
return result;
}
visitPartialFunctionLiteral(node: CSTNode, ctx: PrintContext): string {
const caseClauses = getChildNodes(node, "caseClause");
if (caseClauses.length === 0) {
return "{}";
}
// Single case - try to format on one line if short
if (caseClauses.length === 1) {
const caseStr = this.visitor.visit(caseClauses[0], ctx);
if (caseStr.length < 50) {
return `{ ${caseStr} }`;
}
}
// Multi-line format for long cases or multiple cases
let result = "{\n";
const cases = caseClauses.map(
(c: CSTNode) => " " + this.visitor.visit(c, ctx),
);
result += cases.join("\n");
result += "\n}";
return result;
}
visitAssignmentStatement(node: CSTNode, ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length === 0) {
return "";
}
let result = getNodeImage(identifiers[0]);
// Find the assignment operator
const equals = getChildNodes(node, "Equals");
const plusEquals = getChildNodes(node, "PlusEquals");
const minusEquals = getChildNodes(node, "MinusEquals");
const starEquals = getChildNodes(node, "StarEquals");
const slashEquals = getChildNodes(node, "SlashEquals");
const percentEquals = getChildNodes(node, "PercentEquals");
const sbtAssign = getChildNodes(node, "SbtAssign");
const operator =
equals[0] ||
plusEquals[0] ||
minusEquals[0] ||
starEquals[0] ||
slashEquals[0] ||
percentEquals[0] ||
sbtAssign[0];
if (operator) {
result += " " + getNodeImage(operator) + " ";
const expressions = getChildNodes(node, "expression");
if (expressions.length > 0) {
result += this.visitor.visit(expressions[0], ctx);
}
}
return result;
}
}

View File

@@ -0,0 +1,433 @@
/**
* Scala 3 specific visitor methods for modern language features
*/
import { getChildNodes, getFirstChild, getNodeImage } from "./utils";
import type { PrintContext, CSTNode } from "./utils";
export interface Scala3Visitor {
visit(node: CSTNode, ctx: PrintContext): string;
getIndentation(ctx: PrintContext): string;
visitModifiers(modifiers: CSTNode[], ctx: PrintContext): string;
}
export class Scala3VisitorMethods {
private visitor: Scala3Visitor;
constructor(visitor: Scala3Visitor) {
this.visitor = visitor;
}
// Quote and splice expressions for macros
visitQuoteExpression(node: CSTNode, ctx: PrintContext): string {
const expression = getFirstChild(node, "expression");
return (
"'{ " + (expression ? this.visitor.visit(expression, ctx) : "") + " }"
);
}
visitSpliceExpression(node: CSTNode, ctx: PrintContext): string {
const expression = getFirstChild(node, "expression");
return (
"${ " + (expression ? this.visitor.visit(expression, ctx) : "") + " }"
);
}
// Polymorphic function literals
visitPolymorphicFunctionLiteral(node: CSTNode, ctx: PrintContext): string {
let result = "[";
const polymorphicTypeParams = getChildNodes(
node,
"polymorphicTypeParameter",
);
if (polymorphicTypeParams.length > 0) {
const parameters = polymorphicTypeParams.map((param: CSTNode) =>
this.visitor.visit(param, ctx),
);
result += parameters.join(", ");
}
result += "] => ";
const expression = getFirstChild(node, "expression");
result += expression ? this.visitor.visit(expression, ctx) : "";
return result;
}
// Polymorphic function types
visitPolymorphicFunctionType(node: CSTNode, ctx: PrintContext): string {
let result = "[";
const polymorphicTypeParams = getChildNodes(
node,
"polymorphicTypeParameter",
);
if (polymorphicTypeParams.length > 0) {
const parameters = polymorphicTypeParams.map((param: CSTNode) =>
this.visitor.visit(param, ctx),
);
result += parameters.join(", ");
}
result += "] => ";
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += this.visitor.visit(typeNode, ctx);
}
return result;
}
visitPolymorphicTypeParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Add variance annotation if present
const plusTokens = getChildNodes(node, "Plus");
const minusTokens = getChildNodes(node, "Minus");
if (plusTokens.length > 0) {
result += "+";
} else if (minusTokens.length > 0) {
result += "-";
}
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
result += getNodeImage(identifiers[0]);
}
// Handle type bounds
const subtypeOf = getChildNodes(node, "SubtypeOf");
const supertypeOf = getChildNodes(node, "SupertypeOf");
const typeNode = getFirstChild(node, "type");
if (subtypeOf.length > 0 && typeNode) {
result += " <: " + this.visitor.visit(typeNode, ctx);
}
if (supertypeOf.length > 0 && typeNode) {
result += " >: " + this.visitor.visit(typeNode, ctx);
}
return result;
}
// Enum definitions
visitEnumDefinition(node: CSTNode, ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
let result =
"enum " + (identifiers.length > 0 ? getNodeImage(identifiers[0]) : "");
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const classParameters = getFirstChild(node, "classParameters");
if (classParameters) {
result += this.visitor.visit(classParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
result += " {\n";
const enumCases = getChildNodes(node, "enumCase");
if (enumCases.length > 0) {
const indent = this.visitor.getIndentation(ctx);
const cases = enumCases.map(
(c: CSTNode) => indent + this.visitor.visit(c, ctx),
);
result += cases.join("\n");
}
result += "\n}";
return result;
}
visitEnumCase(node: CSTNode, ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
let result =
"case " + (identifiers.length > 0 ? getNodeImage(identifiers[0]) : "");
const classParameters = getFirstChild(node, "classParameters");
if (classParameters) {
result += this.visitor.visit(classParameters, ctx);
}
const extendsClause = getFirstChild(node, "extendsClause");
if (extendsClause) {
result += " " + this.visitor.visit(extendsClause, ctx);
}
return result;
}
// Extension methods
visitExtensionDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "extension";
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const identifiers = getChildNodes(node, "Identifier");
const typeNode = getFirstChild(node, "type");
result +=
" (" +
(identifiers.length > 0 ? getNodeImage(identifiers[0]) : "") +
": ";
if (typeNode) {
result += this.visitor.visit(typeNode, ctx);
}
result += ") {\n";
const extensionMembers = getChildNodes(node, "extensionMember");
if (extensionMembers.length > 0) {
const members = extensionMembers.map(
(m: CSTNode) => " " + this.visitor.visit(m, ctx),
);
result += members.join("\n");
}
result += "\n}";
return result;
}
visitExtensionMember(node: CSTNode, ctx: PrintContext): string {
const modifierNodes = getChildNodes(node, "modifier");
const modifiers = this.visitor.visitModifiers(modifierNodes, ctx);
const defDefinition = getFirstChild(node, "defDefinition");
const definition = defDefinition
? this.visitor.visit(defDefinition, ctx)
: "";
return modifiers ? modifiers + " " + definition : definition;
}
// Given definitions
visitGivenDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "given";
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
// Named given with potential parameters: given name[T](using ord: Type): Type
result += " " + getNodeImage(identifiers[0]);
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const parameterLists = getFirstChild(node, "parameterLists");
if (parameterLists) {
result += this.visitor.visit(parameterLists, ctx);
}
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
} else {
// Anonymous given: given Type = expression
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += " " + this.visitor.visit(typeNode, ctx);
}
}
const equalsTokens = getChildNodes(node, "Equals");
if (equalsTokens.length > 0) {
const expression = getFirstChild(node, "expression");
if (expression) {
result += " = " + this.visitor.visit(expression, ctx);
}
}
return result;
}
// Type definitions including opaque types
visitTypeDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle opaque types
const opaqueTokens = getChildNodes(node, "Opaque");
if (opaqueTokens.length > 0) {
result += "opaque ";
}
const identifiers = getChildNodes(node, "Identifier");
result +=
"type " + (identifiers.length > 0 ? getNodeImage(identifiers[0]) : "");
const typeParameters = getFirstChild(node, "typeParameters");
if (typeParameters) {
result += this.visitor.visit(typeParameters, ctx);
}
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += " = " + this.visitor.visit(typeNode, ctx);
}
return result;
}
// Export clauses and expressions
visitExportClause(node: CSTNode, ctx: PrintContext): string {
const exportExpression = getFirstChild(node, "exportExpression");
return (
"export " +
(exportExpression ? this.visitor.visit(exportExpression, ctx) : "")
);
}
visitExportExpression(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Build the export path
const identifiers = getChildNodes(node, "Identifier");
const dots = getChildNodes(node, "Dot");
const underscores = getChildNodes(node, "Underscore");
const givens = getChildNodes(node, "Given");
const leftBraces = getChildNodes(node, "LeftBrace");
// Add first identifier
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
// Process remaining parts
let identifierIndex = 1;
for (let i = 0; i < dots.length; i++) {
result += ".";
// Check what follows this dot
if (underscores.length > 0 && i === dots.length - 1) {
// Wildcard export
result += "_";
} else if (givens.length > 0 && i === dots.length - 1) {
// Given export
result += "given";
} else if (leftBraces.length > 0 && i === dots.length - 1) {
// Multiple export selectors
result += "{";
const exportSelectors = getChildNodes(node, "exportSelector");
if (exportSelectors.length > 0) {
const selectors = exportSelectors.map((sel: CSTNode) =>
this.visitor.visit(sel, ctx),
);
result += selectors.join(", ");
}
result += "}";
} else if (identifierIndex < identifiers.length) {
// Next identifier in path
result += getNodeImage(identifiers[identifierIndex]);
identifierIndex++;
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
visitExportSelector(node: CSTNode, _ctx: PrintContext): string {
const underscores = getChildNodes(node, "Underscore");
const identifiers = getChildNodes(node, "Identifier");
const givens = getChildNodes(node, "Given");
const arrows = getChildNodes(node, "Arrow");
// Handle wildcard export
if (underscores.length > 0 && identifiers.length === 0) {
return "_";
}
// Handle given export
if (givens.length > 0 && identifiers.length === 0) {
return "given";
}
let result = "";
// Handle regular identifiers
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
// Handle given with specific identifiers: given SpecificType
if (givens.length > 0 && identifiers.length > 0) {
result = "given " + getNodeImage(identifiers[0]);
}
if (arrows.length > 0) {
result += " => ";
if (underscores.length > 0) {
result += "_";
} else if (identifiers.length > 1) {
result += getNodeImage(identifiers[1]);
}
}
return result;
}
// Context function types
visitContextFunctionType(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle parenthesized types
const leftParens = getChildNodes(node, "LeftParen");
if (leftParens.length > 0) {
const tupleType = getFirstChild(node, "tupleTypeOrParenthesized");
if (tupleType) {
result += "(" + this.visitor.visit(tupleType, ctx) + ")";
}
} else {
// Handle simple types
const simpleType = getFirstChild(node, "simpleType");
if (simpleType) {
result += this.visitor.visit(simpleType, ctx);
}
}
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += " ?=> " + this.visitor.visit(typeNode, ctx);
}
return result;
}
// Inline and transparent modifiers
visitInlineModifier(): string {
return "inline";
}
visitTransparentModifier(): string {
return "transparent";
}
// Using clauses
visitUsingClause(node: CSTNode, ctx: PrintContext): string {
let result = "using ";
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
result += getNodeImage(identifiers[0]);
}
const colonTokens = getChildNodes(node, "Colon");
if (colonTokens.length > 0) {
const typeNode = getFirstChild(node, "type");
if (typeNode) {
result += ": " + this.visitor.visit(typeNode, ctx);
}
}
return result;
}
}

View File

@@ -0,0 +1,658 @@
/**
* Statement visitor methods for import/export, package, and other statements
*/
import { getChildNodes, getFirstChild, getNodeImage } from "./utils";
import type { PrintContext, CSTNode } from "./utils";
export interface StatementVisitor {
visit(node: CSTNode, ctx: PrintContext): string;
}
export class StatementVisitorMethods {
private visitor: StatementVisitor;
constructor(visitor: StatementVisitor) {
this.visitor = visitor;
}
visitPackageClause(node: CSTNode, ctx: PrintContext): string {
const qualifiedIdentifier = getFirstChild(node, "qualifiedIdentifier");
return (
"package " +
(qualifiedIdentifier ? this.visitor.visit(qualifiedIdentifier, ctx) : "")
);
}
visitImportClause(node: CSTNode, ctx: PrintContext): string {
const importExpression = getFirstChild(node, "importExpression");
return (
"import " +
(importExpression ? this.visitor.visit(importExpression, ctx) : "")
);
}
visitImportExpression(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Build the import path
const identifiers = getChildNodes(node, "Identifier");
const dots = getChildNodes(node, "Dot");
// Add first identifier
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
// Process remaining parts
let identifierIndex = 1;
for (let i = 0; i < dots.length; i++) {
result += ".";
// Check what follows this dot
const underscores = getChildNodes(node, "Underscore");
const leftBraces = getChildNodes(node, "LeftBrace");
if (underscores.length > 0 && i === dots.length - 1) {
// Wildcard import
result += "_";
} else if (leftBraces.length > 0 && i === dots.length - 1) {
// Multiple import selectors
result += "{";
const importSelectors = getChildNodes(node, "importSelector");
if (importSelectors.length > 0) {
const selectors = importSelectors.map((sel: CSTNode) =>
this.visitor.visit(sel, ctx),
);
result += selectors.join(", ");
}
result += "}";
} else if (identifierIndex < identifiers.length) {
// Next identifier in path
result += getNodeImage(identifiers[identifierIndex]);
identifierIndex++;
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
visitImportSelector(node: CSTNode, _ctx: PrintContext): string {
// Handle wildcard import
const underscores = getChildNodes(node, "Underscore");
const identifiers = getChildNodes(node, "Identifier");
if (underscores.length > 0 && identifiers.length === 0) {
return "_";
}
let result = "";
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
const arrows = getChildNodes(node, "Arrow");
if (arrows.length > 0) {
result += " => ";
const selectorUnderscores = getChildNodes(node, "Underscore");
if (selectorUnderscores.length > 0) {
result += "_";
} else if (identifiers.length > 1) {
result += getNodeImage(identifiers[1]);
}
}
return result;
}
visitExportClause(node: CSTNode, ctx: PrintContext): string {
const exportExpression = getFirstChild(node, "exportExpression");
return (
"export " +
(exportExpression ? this.visitor.visit(exportExpression, ctx) : "")
);
}
visitExportExpression(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Build the export path
const identifiers = getChildNodes(node, "Identifier");
const dots = getChildNodes(node, "Dot");
// Add first identifier
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
// Process remaining parts
let identifierIndex = 1;
for (let i = 0; i < dots.length; i++) {
result += ".";
// Check what follows this dot
const underscores = getChildNodes(node, "Underscore");
const givens = getChildNodes(node, "Given");
if (underscores.length > 0 && i === dots.length - 1) {
// Wildcard export
result += "_";
} else if (givens.length > 0 && i === dots.length - 1) {
// Given export
result += "given";
} else if (
getChildNodes(node, "LeftBrace").length > 0 &&
i === dots.length - 1
) {
// Multiple export selectors
result += "{";
const exportSelectors = getChildNodes(node, "exportSelector");
if (exportSelectors.length > 0) {
const selectors = exportSelectors.map((sel: CSTNode) =>
this.visitor.visit(sel, ctx),
);
result += selectors.join(", ");
}
result += "}";
} else if (identifierIndex < identifiers.length) {
// Next identifier in path
result += getNodeImage(identifiers[identifierIndex]);
identifierIndex++;
}
}
return result;
}
visitExportSelector(node: CSTNode): string {
const underscores = getChildNodes(node, "Underscore");
const identifiers = getChildNodes(node, "Identifier");
const givens = getChildNodes(node, "Given");
// Handle wildcard export
if (underscores.length > 0 && identifiers.length === 0) {
return "_";
}
// Handle given export
if (givens.length > 0 && identifiers.length === 0) {
return "given";
}
let result = "";
// Handle regular identifiers
if (identifiers.length > 0) {
result = getNodeImage(identifiers[0]);
}
// Handle given with specific identifiers: given SpecificType
if (givens.length > 0 && identifiers.length > 0) {
result = "given " + getNodeImage(identifiers[0]);
}
const arrows = getChildNodes(node, "Arrow");
if (arrows.length > 0) {
result += " => ";
const arrowUnderscores = getChildNodes(node, "Underscore");
if (arrowUnderscores.length > 0) {
result += "_";
} else if (identifiers.length > 1) {
result += getNodeImage(identifiers[1]);
}
}
return result;
}
visitTopLevelDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle modifiers (including 'case')
const modifiers = getChildNodes(node, "modifier");
if (modifiers.length > 0) {
const modifierStr = this.visitModifiers(modifiers, ctx);
result += modifierStr + " ";
}
// Handle definitions at top level
const definition = getFirstChild(node, "definition");
if (definition) {
result += this.visitor.visit(definition, ctx);
return result;
}
// Handle class definitions
const classDefinition = getFirstChild(node, "classDefinition");
if (classDefinition) {
result += this.visitor.visit(classDefinition, ctx);
return result;
}
// Handle object definitions
const objectDefinition = getFirstChild(node, "objectDefinition");
if (objectDefinition) {
result += this.visitor.visit(objectDefinition, ctx);
return result;
}
// Handle trait definitions
const traitDefinition = getFirstChild(node, "traitDefinition");
if (traitDefinition) {
result += this.visitor.visit(traitDefinition, ctx);
return result;
}
// Handle val definitions
const valDefinition = getFirstChild(node, "valDefinition");
if (valDefinition) {
result += this.visitor.visit(valDefinition, ctx);
return result;
}
// Handle var definitions
const varDefinition = getFirstChild(node, "varDefinition");
if (varDefinition) {
result += this.visitor.visit(varDefinition, ctx);
return result;
}
// Handle def definitions
const defDefinition = getFirstChild(node, "defDefinition");
if (defDefinition) {
result += this.visitor.visit(defDefinition, ctx);
return result;
}
// Handle enum definitions (Scala 3)
const enumDefinition = getFirstChild(node, "enumDefinition");
if (enumDefinition) {
result += this.visitor.visit(enumDefinition, ctx);
return result;
}
// Handle extension definitions (Scala 3)
const extensionDefinition = getFirstChild(node, "extensionDefinition");
if (extensionDefinition) {
result += this.visitor.visit(extensionDefinition, ctx);
return result;
}
// Handle given definitions (Scala 3)
const givenDefinition = getFirstChild(node, "givenDefinition");
if (givenDefinition) {
result += this.visitor.visit(givenDefinition, ctx);
return result;
}
// Handle type definitions (including opaque types)
const typeDefinition = getFirstChild(node, "typeDefinition");
if (typeDefinition) {
result += this.visitor.visit(typeDefinition, ctx);
return result;
}
// Handle assignment statements
const assignmentStatement = getFirstChild(node, "assignmentStatement");
if (assignmentStatement) {
result += this.visitor.visit(assignmentStatement, ctx);
return result;
}
// Handle expressions
const expression = getFirstChild(node, "expression");
if (expression) {
result += this.visitor.visit(expression, ctx);
return result;
}
return result;
}
visitBlockStatement(node: CSTNode, ctx: PrintContext): string {
const valDefinition = getFirstChild(node, "valDefinition");
if (valDefinition) {
return this.visitor.visit(valDefinition, ctx);
}
const varDefinition = getFirstChild(node, "varDefinition");
if (varDefinition) {
return this.visitor.visit(varDefinition, ctx);
}
const defDefinition = getFirstChild(node, "defDefinition");
if (defDefinition) {
return this.visitor.visit(defDefinition, ctx);
}
const assignmentStatement = getFirstChild(node, "assignmentStatement");
if (assignmentStatement) {
return this.visitor.visit(assignmentStatement, ctx);
}
const expression = getFirstChild(node, "expression");
if (expression) {
return this.visitor.visit(expression, ctx);
}
return "";
}
visitCompilationUnit(node: CSTNode, ctx: PrintContext): string {
const parts: string[] = [];
// Add package clause if it exists
const packageClause = getFirstChild(node, "packageClause");
if (packageClause) {
parts.push(this.visitor.visit(packageClause, ctx));
}
// Add empty line after package
if (parts.length > 0) {
parts.push("");
}
// Add import clauses
const importClauses = getChildNodes(node, "importClause");
if (importClauses.length > 0) {
importClauses.forEach((importNode: CSTNode) => {
parts.push(this.visitor.visit(importNode, ctx));
});
}
// Add empty line after imports
if (importClauses.length > 0) {
parts.push("");
}
// Add export clauses
const exportClauses = getChildNodes(node, "exportClause");
if (exportClauses.length > 0) {
exportClauses.forEach((exportNode: CSTNode) => {
parts.push(this.visitor.visit(exportNode, ctx));
});
}
// Don't add empty line after exports unless there are subsequent elements
if (exportClauses.length > 0) {
// Only add empty line if there are other elements after exports
const topLevelDefinitions = getChildNodes(node, "topLevelDefinition");
const topLevelStatements = getChildNodes(node, "topLevelStatement");
const expressions = getChildNodes(node, "expression");
const hasSubsequentElements =
topLevelDefinitions.length > 0 ||
topLevelStatements.length > 0 ||
expressions.length > 0;
if (hasSubsequentElements) {
parts.push("");
}
}
// Add top-level definitions
const topLevelDefinitions = getChildNodes(node, "topLevelDefinition");
if (topLevelDefinitions.length > 0) {
topLevelDefinitions.forEach((def: CSTNode) => {
parts.push(this.visitor.visit(def, ctx));
});
}
// Add top-level statements
const topLevelStatements = getChildNodes(node, "topLevelStatement");
if (topLevelStatements.length > 0) {
topLevelStatements.forEach((stmt: CSTNode) => {
parts.push(this.visitor.visit(stmt, ctx));
});
}
// Add top-level expressions
const expressions = getChildNodes(node, "expression");
if (expressions.length > 0) {
expressions.forEach((expr: CSTNode) => {
parts.push(this.visitor.visit(expr, ctx));
});
}
// Join parts and ensure proper file formatting
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0] + "\n";
// For multiple parts, join with newlines and add trailing newline
return parts.join("\n") + "\n";
}
visitAnnotations(annotations: CSTNode[], ctx: PrintContext): string {
return annotations.map((ann) => this.visitor.visit(ann, ctx)).join(" ");
}
visitAnnotation(node: CSTNode, ctx: PrintContext): string {
const qualifiedIdentifier = getFirstChild(node, "qualifiedIdentifier");
let result =
"@" +
(qualifiedIdentifier ? this.visitor.visit(qualifiedIdentifier, ctx) : "");
// Handle multiple parameter lists: @Inject() or @Inject()(val x: Type)
const leftParens = getChildNodes(node, "LeftParen");
if (leftParens.length > 0) {
const annotationArguments = getChildNodes(node, "annotationArgument");
let argIndex = 0;
// Process each parameter list
for (let i = 0; i < leftParens.length; i++) {
result += "(";
// Determine how many arguments are in this parameter list
// We need to group arguments by parameter list
const argsInThisList: CSTNode[] = [];
// For simplicity, distribute arguments evenly across parameter lists
// In practice, this should be based on actual parsing structure
const argsPerList = Math.ceil(
annotationArguments.length / leftParens.length,
);
const endIndex = Math.min(
argIndex + argsPerList,
annotationArguments.length,
);
for (let j = argIndex; j < endIndex; j++) {
argsInThisList.push(annotationArguments[j]);
}
argIndex = endIndex;
if (argsInThisList.length > 0) {
const args = argsInThisList.map((arg: CSTNode) =>
this.visitor.visit(arg, ctx),
);
result += args.join(", ");
}
result += ")";
}
}
return result;
}
visitAnnotationArgument(node: CSTNode, ctx: PrintContext): string {
const valTokens = getChildNodes(node, "Val");
const varTokens = getChildNodes(node, "Var");
const identifiers = getChildNodes(node, "Identifier");
const colons = getChildNodes(node, "Colon");
const equals = getChildNodes(node, "Equals");
const expressions = getChildNodes(node, "expression");
const types = getChildNodes(node, "type");
// Parameter declaration: val x: Type or var y: Type
if (
(valTokens.length > 0 || varTokens.length > 0) &&
identifiers.length > 0 &&
colons.length > 0 &&
types.length > 0
) {
let result = valTokens.length > 0 ? "val " : "var ";
result += getNodeImage(identifiers[0]);
result += ": ";
result += this.visitor.visit(types[0], ctx);
// Optional default value
if (equals.length > 0 && expressions.length > 0) {
result += " = " + this.visitor.visit(expressions[0], ctx);
}
return result;
}
// Named argument: name = value
else if (
identifiers.length > 0 &&
equals.length > 0 &&
expressions.length > 0
) {
return (
getNodeImage(identifiers[0]) +
" = " +
this.visitor.visit(expressions[0], ctx)
);
}
// Positional argument
else if (expressions.length > 0) {
return this.visitor.visit(expressions[0], ctx);
}
return "";
}
visitModifiers(modifiers: CSTNode[], ctx: PrintContext): string {
return modifiers.map((mod) => this.visitor.visit(mod, ctx)).join(" ");
}
visitDefinition(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle annotations
const annotations = getChildNodes(node, "annotation");
if (annotations.length > 0) {
const annotationStr = this.visitAnnotations(annotations, ctx);
result += annotationStr + " ";
}
// Handle modifiers
const modifiers = getChildNodes(node, "modifier");
if (modifiers.length > 0) {
const modifierStr = this.visitModifiers(modifiers, ctx);
result += modifierStr + " ";
}
// Handle specific definition types
const classDefinition = getFirstChild(node, "classDefinition");
if (classDefinition) {
result += this.visitor.visit(classDefinition, ctx);
} else {
const objectDefinition = getFirstChild(node, "objectDefinition");
if (objectDefinition) {
result += this.visitor.visit(objectDefinition, ctx);
} else {
const traitDefinition = getFirstChild(node, "traitDefinition");
if (traitDefinition) {
result += this.visitor.visit(traitDefinition, ctx);
} else {
const enumDefinition = getFirstChild(node, "enumDefinition");
if (enumDefinition) {
result += this.visitor.visit(enumDefinition, ctx);
} else {
const extensionDefinition = getFirstChild(
node,
"extensionDefinition",
);
if (extensionDefinition) {
result += this.visitor.visit(extensionDefinition, ctx);
} else {
const valDefinition = getFirstChild(node, "valDefinition");
if (valDefinition) {
result += this.visitor.visit(valDefinition, ctx);
} else {
const varDefinition = getFirstChild(node, "varDefinition");
if (varDefinition) {
result += this.visitor.visit(varDefinition, ctx);
} else {
const defDefinition = getFirstChild(node, "defDefinition");
if (defDefinition) {
result += this.visitor.visit(defDefinition, ctx);
} else {
const givenDefinition = getFirstChild(
node,
"givenDefinition",
);
if (givenDefinition) {
result += this.visitor.visit(givenDefinition, ctx);
} else {
const typeDefinition = getFirstChild(
node,
"typeDefinition",
);
if (typeDefinition) {
result += this.visitor.visit(typeDefinition, ctx);
} else {
const assignmentStatement = getFirstChild(
node,
"assignmentStatement",
);
if (assignmentStatement) {
result += this.visitor.visit(
assignmentStatement,
ctx,
);
}
}
}
}
}
}
}
}
}
}
}
return result;
}
visitPattern(node: CSTNode, ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
return getNodeImage(identifiers[0]);
}
const underscores = getChildNodes(node, "Underscore");
if (underscores.length > 0) {
return "_";
}
const literal = getFirstChild(node, "literal");
if (literal) {
return this.visitor.visit(literal, ctx);
}
const leftParens = getChildNodes(node, "LeftParen");
if (leftParens.length > 0) {
// Tuple pattern or parenthesized pattern
const patterns = getChildNodes(node, "pattern");
if (patterns.length > 1) {
const patternResults = patterns.map((p: CSTNode) =>
this.visitor.visit(p, ctx),
);
return "(" + patternResults.join(", ") + ")";
} else if (patterns.length === 1) {
return "(" + this.visitor.visit(patterns[0], ctx) + ")";
}
}
const patterns = getChildNodes(node, "pattern");
if (patterns.length > 0) {
// Constructor pattern or other complex patterns
return this.visitor.visit(patterns[0], ctx);
}
return "";
}
}

View File

@@ -0,0 +1,474 @@
/**
* Type-related visitor methods for handling type expressions, type parameters, and type systems
*/
import { getChildNodes, getFirstChild, getNodeImage } from "./utils";
import type { PrintContext, CSTNode } from "./utils";
export interface TypeVisitor {
visit(node: CSTNode, ctx: PrintContext): string;
}
export class TypeVisitorMethods {
private visitor: TypeVisitor;
constructor(visitor: TypeVisitor) {
this.visitor = visitor;
}
visitType(node: CSTNode, ctx: PrintContext): string {
const matchType = getFirstChild(node, "matchType");
return matchType ? this.visitor.visit(matchType, ctx) : "";
}
visitMatchType(node: CSTNode, ctx: PrintContext): string {
const unionType = getFirstChild(node, "unionType");
let result = unionType ? this.visitor.visit(unionType, ctx) : "";
const matchTokens = getChildNodes(node, "Match");
if (matchTokens.length > 0) {
result += " match {";
const matchTypeCases = getChildNodes(node, "matchTypeCase");
if (matchTypeCases.length > 0) {
for (const caseNode of matchTypeCases) {
result += "\n " + this.visitor.visit(caseNode, ctx);
}
result += "\n";
}
result += "}";
}
return result;
}
visitMatchTypeCase(node: CSTNode, ctx: PrintContext): string {
const types = getChildNodes(node, "type");
if (types.length >= 2) {
const leftType = this.visitor.visit(types[0], ctx);
const rightType = this.visitor.visit(types[1], ctx);
return `case ${leftType} => ${rightType}`;
}
return "";
}
visitUnionType(node: CSTNode, ctx: PrintContext): string {
const types = getChildNodes(node, "intersectionType");
if (types.length === 1) {
return this.visitor.visit(types[0], ctx);
}
const typeStrings = types.map((t: CSTNode) => this.visitor.visit(t, ctx));
return typeStrings.join(" | ");
}
visitIntersectionType(node: CSTNode, ctx: PrintContext): string {
const types = getChildNodes(node, "baseType");
if (types.length === 1) {
return this.visitor.visit(types[0], ctx);
}
const typeStrings = types.map((t: CSTNode) => this.visitor.visit(t, ctx));
return typeStrings.join(" & ");
}
visitContextFunctionType(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle parenthesized types
const leftParen = getChildNodes(node, "LeftParen");
if (leftParen.length > 0) {
const tupleType = getFirstChild(node, "tupleTypeOrParenthesized");
if (tupleType) {
result += "(" + this.visitor.visit(tupleType, ctx) + ")";
}
} else {
// Handle simple types
const simpleType = getFirstChild(node, "simpleType");
if (simpleType) {
result += this.visitor.visit(simpleType, ctx);
}
}
const type = getFirstChild(node, "type");
if (type) {
result += " ?=> " + this.visitor.visit(type, ctx);
}
return result;
}
visitBaseType(node: CSTNode, ctx: PrintContext): string {
// Handle type lambda: [X] =>> F[X]
const typeLambda = getFirstChild(node, "typeLambda");
if (typeLambda) {
return this.visitor.visit(typeLambda, ctx);
}
// Handle polymorphic function type: [T] => T => T
const polymorphicFunctionType = getFirstChild(
node,
"polymorphicFunctionType",
);
if (polymorphicFunctionType) {
return this.visitor.visit(polymorphicFunctionType, ctx);
}
// Handle context function type: String ?=> Int
const contextFunctionType = getFirstChild(node, "contextFunctionType");
if (contextFunctionType) {
return this.visitor.visit(contextFunctionType, ctx);
}
// Handle dependent function type: (x: Int) => Vector[x.type]
const dependentFunctionType = getFirstChild(node, "dependentFunctionType");
if (dependentFunctionType) {
return this.visitor.visit(dependentFunctionType, ctx);
}
// Handle parenthesized types or tuple types: (String | Int) or (A, B)
const leftParen = getChildNodes(node, "LeftParen");
const tupleType = getFirstChild(node, "tupleTypeOrParenthesized");
if (leftParen.length > 0 && tupleType) {
return "(" + this.visitor.visit(tupleType, ctx) + ")";
}
// Handle simple types with array notation
const simpleType = getFirstChild(node, "simpleType");
let result = "";
if (simpleType) {
result = this.visitor.visit(simpleType, ctx);
} else {
// Handle direct token cases like Array, List, etc.
if ("children" in node && node.children) {
const children = node.children;
for (const [key, tokens] of Object.entries(children)) {
if (
Array.isArray(tokens) &&
tokens.length > 0 &&
"image" in tokens[0]
) {
// Check if this is a type name token (not brackets or keywords)
if (
!["LeftBracket", "RightBracket", "typeArgument"].includes(key)
) {
result = getNodeImage(tokens[0]);
break;
}
}
}
}
}
if (!result) {
return "";
}
// Handle array types like Array[String]
const leftBrackets = getChildNodes(node, "LeftBracket");
const typeArguments = getChildNodes(node, "typeArgument");
for (let i = 0; i < leftBrackets.length && i < typeArguments.length; i++) {
result += "[" + this.visitor.visit(typeArguments[i], ctx) + "]";
}
return result;
}
visitTupleTypeOrParenthesized(node: CSTNode, ctx: PrintContext): string {
const types = getChildNodes(node, "type");
if (types.length === 1) {
return this.visitor.visit(types[0], ctx);
}
const typeStrings = types.map((t: CSTNode) => this.visitor.visit(t, ctx));
return typeStrings.join(", ");
}
visitSimpleType(node: CSTNode, ctx: PrintContext): string {
const qualifiedId = getFirstChild(node, "qualifiedIdentifier");
if (!qualifiedId) {
return "";
}
let result = this.visitor.visit(qualifiedId, ctx);
// Handle type parameters like List[Int] or Kind Projector like Map[String, *]
const leftBrackets = getChildNodes(node, "LeftBracket");
if (leftBrackets.length > 0) {
const typeArgs = getChildNodes(node, "typeArgument");
const typeStrings = typeArgs.map((t: CSTNode) =>
this.visitor.visit(t, ctx),
);
result += "[" + typeStrings.join(", ") + "]";
}
return result;
}
visitTypeArgument(node: CSTNode, ctx: PrintContext): string {
// Handle Kind Projector notation: *
const star = getChildNodes(node, "Star");
if (star.length > 0) {
return "*";
}
// Handle regular type
const type = getFirstChild(node, "type");
if (type) {
return this.visitor.visit(type, ctx);
}
// Handle type argument union structure
const typeArgumentUnion = getFirstChild(node, "typeArgumentUnion");
if (typeArgumentUnion) {
return this.visitor.visit(typeArgumentUnion, ctx);
}
// Handle direct type tokens like Array[t] within type arguments
if ("children" in node && node.children) {
const children = node.children;
let result = "";
// Find the type name token
for (const [key, tokens] of Object.entries(children)) {
if (
Array.isArray(tokens) &&
tokens.length > 0 &&
"image" in tokens[0]
) {
if (!["LeftBracket", "RightBracket", "typeArgument"].includes(key)) {
result = getNodeImage(tokens[0]);
break;
}
}
}
if (result) {
// Handle type parameters like Array[t] within type arguments
const leftBrackets = getChildNodes(node, "LeftBracket");
const typeArguments = getChildNodes(node, "typeArgument");
for (
let i = 0;
i < leftBrackets.length && i < typeArguments.length;
i++
) {
result += "[" + this.visitor.visit(typeArguments[i], ctx) + "]";
}
return result;
}
}
return "";
}
visitTypeLambda(node: CSTNode, ctx: PrintContext): string {
let result = "[";
const parameters = getChildNodes(node, "typeLambdaParameter");
if (parameters.length > 0) {
const parameterStrings = parameters.map((param: CSTNode) =>
this.visitor.visit(param, ctx),
);
result += parameterStrings.join(", ");
}
result += "] =>> ";
const type = getFirstChild(node, "type");
if (type) {
result += this.visitor.visit(type, ctx);
}
return result;
}
visitTypeLambdaParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Add variance annotation if present
const plus = getChildNodes(node, "Plus");
const minus = getChildNodes(node, "Minus");
if (plus.length > 0) {
result += "+";
} else if (minus.length > 0) {
result += "-";
}
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
result += getNodeImage(identifiers[0]);
}
const subtypeOf = getChildNodes(node, "SubtypeOf");
const supertypeOf = getChildNodes(node, "SupertypeOf");
const type = getFirstChild(node, "type");
if (subtypeOf.length > 0 && type) {
result += " <: " + this.visitor.visit(type, ctx);
} else if (supertypeOf.length > 0 && type) {
result += " >: " + this.visitor.visit(type, ctx);
}
return result;
}
visitDependentFunctionType(node: CSTNode, ctx: PrintContext): string {
let result = "(";
const parameters = getChildNodes(node, "dependentParameter");
if (parameters.length > 0) {
const parameterStrings = parameters.map((param: CSTNode) =>
this.visitor.visit(param, ctx),
);
result += parameterStrings.join(", ");
}
result += ") => ";
const type = getFirstChild(node, "type");
if (type) {
result += this.visitor.visit(type, ctx);
}
return result;
}
visitDependentParameter(node: CSTNode, ctx: PrintContext): string {
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length === 0) {
return "";
}
let result = getNodeImage(identifiers[0]);
const type = getFirstChild(node, "type");
if (type) {
result += ": " + this.visitor.visit(type, ctx);
}
return result;
}
visitPolymorphicFunctionType(node: CSTNode, ctx: PrintContext): string {
let result = "[";
const parameters = getChildNodes(node, "polymorphicTypeParameter");
if (parameters.length > 0) {
const parameterStrings = parameters.map((param: CSTNode) =>
this.visitor.visit(param, ctx),
);
result += parameterStrings.join(", ");
}
result += "] => ";
const type = getFirstChild(node, "type");
if (type) {
result += this.visitor.visit(type, ctx);
}
return result;
}
visitPolymorphicTypeParameter(node: CSTNode, ctx: PrintContext): string {
let result = "";
// Handle variance annotation
const plus = getChildNodes(node, "Plus");
const minus = getChildNodes(node, "Minus");
if (plus.length > 0) {
result += "+";
} else if (minus.length > 0) {
result += "-";
}
const identifiers = getChildNodes(node, "Identifier");
if (identifiers.length > 0) {
result += getNodeImage(identifiers[0]);
}
// Handle type bounds
const subtypeOf = getChildNodes(node, "SubtypeOf");
const supertypeOf = getChildNodes(node, "SupertypeOf");
const type = getFirstChild(node, "type");
if (subtypeOf.length > 0 && type) {
result += " <: " + this.visitor.visit(type, ctx);
}
if (supertypeOf.length > 0 && type) {
result += " >: " + this.visitor.visit(type, ctx);
}
return result;
}
visitTypeArgumentUnion(node: CSTNode, ctx: PrintContext): string {
const typeArgumentIntersections = getChildNodes(
node,
"typeArgumentIntersection",
);
if (typeArgumentIntersections.length === 1) {
return this.visitor.visit(typeArgumentIntersections[0], ctx);
}
// Handle union types with | operator
if (typeArgumentIntersections.length > 1) {
const typeStrings = typeArgumentIntersections.map((t: CSTNode) =>
this.visitor.visit(t, ctx),
);
return typeStrings.join(" | ");
}
return "";
}
visitTypeArgumentIntersection(node: CSTNode, ctx: PrintContext): string {
const typeArgumentSimples = getChildNodes(node, "typeArgumentSimple");
if (typeArgumentSimples.length === 1) {
return this.visitor.visit(typeArgumentSimples[0], ctx);
}
// Handle intersection types with & operator
if (typeArgumentSimples.length > 1) {
const typeStrings = typeArgumentSimples.map((t: CSTNode) =>
this.visitor.visit(t, ctx),
);
return typeStrings.join(" & ");
}
return "";
}
visitTypeArgumentSimple(node: CSTNode, ctx: PrintContext): string {
const qualifiedIdentifier = getFirstChild(node, "qualifiedIdentifier");
if (qualifiedIdentifier) {
let result = this.visitor.visit(qualifiedIdentifier, ctx);
// Handle type parameters like List[*] within type arguments
const leftBrackets = getChildNodes(node, "LeftBracket");
if (leftBrackets.length > 0) {
const typeArgs = getChildNodes(node, "typeArgument");
const typeStrings = typeArgs.map((t: CSTNode) =>
this.visitor.visit(t, ctx),
);
result += "[" + typeStrings.join(", ") + "]";
}
return result;
}
// Handle simple type structures like List[*]
const simpleType = getFirstChild(node, "simpleType");
if (simpleType) {
return this.visitor.visit(simpleType, ctx);
}
// Handle base type structures
const baseType = getFirstChild(node, "baseType");
if (baseType) {
return this.visitor.visit(baseType, ctx);
}
// Handle other type argument patterns
const identifier = getFirstChild(node, "Identifier");
if (identifier) {
return getNodeImage(identifier);
}
return "";
}
}

View File

@@ -0,0 +1,295 @@
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;
}

View File

@@ -0,0 +1,72 @@
/**
* Prettier Plugin for TOML file formatting
*
* This plugin provides support for formatting TOML (Tom's Obvious, Minimal Language) files
* using the @toml-tools/parser and custom beautifier.
*/
import type { Plugin, Parser, Printer, SupportLanguage, SupportOption } from 'prettier';
import { parse } from '@toml-tools/parser';
import { locStart, locEnd } from './loc';
import { print } from './printer';
import type { TomlDocument, TomlCstNode } from './types';
const parserName = 'toml';
// https://prettier.io/docs/en/plugins.html#languages
const languages: SupportLanguage[] = [
{
extensions: ['.toml'],
name: 'Toml',
parsers: [parserName],
filenames: ['Cargo.lock', 'Gopkg.lock'],
tmScope: 'source.toml',
aceMode: 'toml',
codemirrorMode: 'toml',
codemirrorMimeType: 'text/x-toml',
linguistLanguageId: 365,
vscodeLanguageIds: ['toml'],
},
];
// https://prettier.io/docs/en/plugins.html#parsers
const tomlParser: Parser<TomlDocument> = {
astFormat: 'toml-cst',
parse: (text: string): TomlDocument => {
try {
return parse(text) as TomlDocument;
} catch (error) {
console.error('TOML parsing error:', error);
throw error;
}
},
locStart,
locEnd,
};
// https://prettier.io/docs/en/plugins.html#printers
const tomlPrinter: Printer<TomlCstNode> = {
print,
};
// Plugin options
const options: Record<string, SupportOption> = {
};
// Plugin definition
const tomlPlugin: Plugin = {
languages,
parsers: {
[parserName]: tomlParser,
},
printers: {
'toml-cst': tomlPrinter,
},
options,
};
export default tomlPlugin;
export { languages };
export const parsers = tomlPlugin.parsers;
export const printers = tomlPlugin.printers;

View File

@@ -0,0 +1,82 @@
/**
* Location utilities for TOML CST nodes
* These functions help Prettier determine the location of nodes for formatting
*/
import type { TomlCstNode } from './types';
/**
* Get the start location of a CST node
* @param cstNode - The TOML CST node
* @returns The start offset of the node
*/
export function locStart(cstNode: TomlCstNode): number {
if (!cstNode) {
return 0;
}
// If the node has a direct startOffset, use it
if (typeof cstNode.startOffset === 'number') {
return cstNode.startOffset;
}
// If the node has children, find the earliest start offset
if (cstNode.children) {
let minOffset = Infinity;
for (const key in cstNode.children) {
const childrenArray = cstNode.children[key];
if (Array.isArray(childrenArray)) {
for (const child of childrenArray) {
const childStart = locStart(child);
if (childStart < minOffset) {
minOffset = childStart;
}
}
}
}
return minOffset === Infinity ? 0 : minOffset;
}
return 0;
}
/**
* Get the end location of a CST node
* @param cstNode - The TOML CST node
* @returns The end offset of the node
*/
export function locEnd(cstNode: TomlCstNode): number {
if (!cstNode) {
return 0;
}
// If the node has a direct endOffset, use it
if (typeof cstNode.endOffset === 'number') {
return cstNode.endOffset;
}
// If the node has children, find the latest end offset
if (cstNode.children) {
let maxOffset = -1;
for (const key in cstNode.children) {
const childrenArray = cstNode.children[key];
if (Array.isArray(childrenArray)) {
for (const child of childrenArray) {
const childEnd = locEnd(child);
if (childEnd > maxOffset) {
maxOffset = childEnd;
}
}
}
}
return maxOffset === -1 ? 0 : maxOffset;
}
// If the node has an image (token), return the length
if (cstNode.image) {
const startOffset = locStart(cstNode);
return startOffset + cstNode.image.length;
}
return 0;
}

View File

@@ -0,0 +1,284 @@
/**
* Utility functions for TOML printer
*/
import type { TomlCstNode, TomlComment, TomlContext } from './types';
/**
* Trim trailing whitespace from comment text
* @param commentText - The comment text to trim
* @returns Trimmed comment text
*/
export function trimComment(commentText: string): string {
return commentText.replace(/[ \t]+$/, '');
}
/**
* Check if a quoted string can be unquoted
* @param quotedText - The quoted text to check
* @returns Whether the text can be unquoted
*/
export function canUnquote(quotedText: string): boolean {
// Remove quotes if present
let text = quotedText;
if (text.startsWith('"') && text.endsWith('"')) {
text = text.slice(1, -1);
} else if (text.startsWith("'") && text.endsWith("'")) {
text = text.slice(1, -1);
}
// Empty string needs quotes
if (text.length === 0) {
return false;
}
// Check if the string is a valid unquoted key
// TOML unquoted keys can contain:
// - A-Z, a-z, 0-9, _, -
const unquotedKeyRegex = /^[A-Za-z0-9_-]+$/;
// Additional checks for values that might be confused with other TOML types
if (unquotedKeyRegex.test(text)) {
// Don't unquote strings that look like booleans
if (text === 'true' || text === 'false') {
return false;
}
// Don't unquote strings that look like numbers
if (/^[+-]?(\d+\.?\d*|\d*\.\d+)([eE][+-]?\d+)?$/.test(text)) {
return false;
}
// Don't unquote strings that look like dates/times
if (/^\d{4}-\d{2}-\d{2}/.test(text)) {
return false;
}
return true;
}
return false;
}
/**
* Check if a key needs quotes
* @param keyText - The key text to check
* @returns Whether the key needs quotes
*/
export function keyNeedsQuotes(keyText: string): boolean {
return !canUnquote(`"${keyText}"`);
}
/**
* Format a key, adding or removing quotes as needed
* @param keyText - The key text to format
* @returns Formatted key
*/
export function formatKey(keyText: string): string {
// If already quoted, check if we can unquote
if ((keyText.startsWith('"') && keyText.endsWith('"')) ||
(keyText.startsWith("'") && keyText.endsWith("'"))) {
if (canUnquote(keyText)) {
return keyText.slice(1, -1);
}
return keyText;
}
// If not quoted, check if we need to add quotes
if (keyNeedsQuotes(keyText)) {
return `"${keyText}"`;
}
return keyText;
}
/**
* Check if a string contains escape sequences that need to be preserved
* @param str - The string to check
* @returns Whether the string contains escape sequences
*/
export function containsEscapeSequences(str: string): boolean {
// Check for common escape sequences
return /\\[btnfr"\\\/]|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}/.test(str);
}
/**
* Check if a string can use literal string syntax (single quotes)
* @param str - The string to check (without quotes)
* @returns Whether literal string syntax can be used
*/
export function canUseLiteralString(str: string): boolean {
// Literal strings cannot contain single quotes or control characters
// and don't need escape sequences
return !str.includes("'") &&
!/[\x00-\x08\x0A-\x1F\x7F]/.test(str) &&
!containsEscapeSequences(str);
}
/**
* Check if a string should use multiline syntax
* @param str - The string to check (without quotes)
* @returns Whether multiline syntax should be used
*/
export function shouldUseMultiline(str: string): boolean {
// Use multiline for strings that contain newlines
return str.includes('\n') || str.includes('\r');
}
/**
* Format a string value optimally
* @param value - The string value (potentially with quotes)
* @returns Optimally formatted string
*/
export function formatStringValue(value: string): string {
// If it's already a properly formatted string, keep it
if (!value.startsWith('"') && !value.startsWith("'")) {
return value;
}
// Extract the actual string content
let content: string;
let isLiteral = false;
if (value.startsWith('"""') && value.endsWith('"""')) {
// Multiline basic string
content = value.slice(3, -3);
} else if (value.startsWith("'''") && value.endsWith("'''")) {
// Multiline literal string
content = value.slice(3, -3);
isLiteral = true;
} else if (value.startsWith('"') && value.endsWith('"')) {
// Basic string
content = value.slice(1, -1);
} else if (value.startsWith("'") && value.endsWith("'")) {
// Literal string
content = value.slice(1, -1);
isLiteral = true;
} else {
return value; // Fallback
}
// Decide on the best format
if (shouldUseMultiline(content)) {
if (isLiteral || !containsEscapeSequences(content)) {
// Use multiline literal string if no escapes needed
return `'''${content}'''`;
} else {
// Use multiline basic string
return `"""${content}"""`;
}
} else {
if (canUseLiteralString(content) && !containsEscapeSequences(content)) {
// Use literal string for simple cases
return `'${content}'`;
} else {
// Use basic string
return `"${content}"`;
}
}
}
/**
* Optimize value representation (for strings, numbers, etc.)
* @param value - The value to optimize
* @returns Optimized value representation
*/
export function optimizeValue(value: string): string {
// Handle string values
if (value.startsWith('"') || value.startsWith("'")) {
return formatStringValue(value);
}
// For non-strings, return as-is
return value;
}
/**
* Collect all comments from comment newline nodes
* @param commentsNL - Array of comment newline nodes
* @returns Array of comment tokens
*/
export function collectComments(commentsNL: TomlCstNode[] = []): TomlComment[] {
const comments: TomlComment[] = [];
commentsNL.forEach((commentNLNode) => {
if (commentNLNode.children?.Comment) {
const commentsTok = commentNLNode.children.Comment;
for (const comment of commentsTok) {
if (comment.image) {
comments.push(comment as TomlComment);
}
}
}
});
return comments;
}
/**
* Get a single element from a context that should contain exactly one key-value pair
* @param ctx - The context to extract from
* @returns The single element
* @throws Error if the context doesn't contain exactly one element
*/
export function getSingle(ctx: TomlContext): TomlCstNode {
const ctxKeys = Object.keys(ctx);
if (ctxKeys.length !== 1) {
throw new Error(
`Expecting single key CST ctx but found: <${ctxKeys.length}> keys`
);
}
const singleElementKey = ctxKeys[0];
const singleElementValues = ctx[singleElementKey];
if (!Array.isArray(singleElementValues) || singleElementValues.length !== 1) {
throw new Error(
`Expecting single item in CST ctx key but found: <${singleElementValues?.length || 0}> items`
);
}
return singleElementValues[0];
}
/**
* Get the start offset of an array item (deprecated - use arrItemProp instead)
* @param item - The array item node
* @returns The start offset
*/
export function arrItemOffset(item: TomlCstNode): number {
return arrItemProp(item, 'startOffset') as number;
}
/**
* Get a specific property from an array item, handling wrapped values
* @param item - The array item node
* @param propName - The property name to retrieve
* @returns The property value
* @throws Error for non-exhaustive matches
*/
export function arrItemProp(item: TomlCstNode, propName: keyof TomlCstNode): any {
let currentItem = item;
// Unwrap 'val' nodes
if (currentItem.name === 'val' && currentItem.children) {
currentItem = getSingle(currentItem.children);
}
// Direct property access
if (currentItem[propName] !== undefined) {
return currentItem[propName];
}
// Check for LSquare (array start)
if (currentItem.children?.LSquare?.[0]?.[propName] !== undefined) {
return currentItem.children.LSquare[0][propName];
}
// Check for LCurly (inline table start)
if (currentItem.children?.LCurly?.[0]?.[propName] !== undefined) {
return currentItem.children.LCurly[0][propName];
}
throw new Error(`Non-exhaustive match for property ${propName}`);
}

View File

@@ -0,0 +1,413 @@
/**
* TOML Printer for Prettier
*
* This module provides a visitor-based printer for TOML CST nodes,
* converting them to Prettier's document format.
*/
import { BaseTomlCstVisitor } from '@toml-tools/parser';
import { tokensDictionary as t } from '@toml-tools/lexer';
import { doc } from 'prettier';
import type { AstPath, Doc } from 'prettier';
import {
trimComment,
collectComments,
arrItemOffset,
arrItemProp,
getSingle,
formatKey,
optimizeValue,
} from './printer-utils';
import type {
TomlCstNode,
TomlDocument,
TomlExpression,
TomlKeyVal,
TomlComment,
TomlContext
} from './types';
const { join, line, hardline, softline, ifBreak, indent, group } = doc.builders;
/**
* TOML Beautifier Visitor class that extends the base CST visitor
*/
class TomlBeautifierVisitor extends BaseTomlCstVisitor {
// Helper methods
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
public visitSingle: (ctx: TomlContext) => Doc | string;
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
constructor() {
super();
// Try to call validateVisitor if it exists
if (typeof (this as any).validateVisitor === 'function') {
(this as any).validateVisitor();
}
// Initialize helper methods
this.mapVisit = (elements: TomlCstNode[] | undefined): (Doc | string)[] => {
if (!elements) {
return [];
}
return elements.map((element) => this.visit(element));
};
this.visitSingle = (ctx: TomlContext): Doc | string => {
const singleElement = getSingle(ctx);
return this.visit(singleElement);
};
// Store reference to inherited visit method and override it
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this);
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => {
if (!ctx) {
return '';
}
// Try to use the inherited visit method first
if (originalVisit) {
try {
return originalVisit(ctx, inParam);
} catch (error) {
console.warn('Original visit method failed:', error);
}
}
// Fallback: manually dispatch based on node name/type
const methodName = ctx.name;
if (methodName && typeof (this as any)[methodName] === 'function') {
const visitMethod = (this as any)[methodName];
try {
if (ctx.children) {
return visitMethod.call(this, ctx.children);
} else {
return visitMethod.call(this, ctx);
}
} catch (error) {
console.warn(`Visit method ${methodName} failed:`, error);
}
}
// Final fallback: return image if available
return ctx.image || '';
};
}
/**
* Visit the root TOML document
*/
toml(ctx: TomlDocument): Doc {
// Handle empty toml document
if (!ctx.expression) {
return [line];
}
const isTable = (node: TomlExpression): boolean => {
return !!node.table;
};
const isOnlyComment = (node: TomlExpression): boolean => {
return !!node.Comment && Object.keys(node).length === 1;
};
const expsCsts = ctx.expression;
const cstGroups: TomlExpression[][] = [];
let currCstGroup: TomlExpression[] = [];
// Split expressions into groups defined by tables
for (let i = expsCsts.length - 1; i >= 0; i--) {
const currCstNode = expsCsts[i];
currCstGroup.push(currCstNode);
if (isTable(currCstNode)) {
let j = i - 1;
let stillInComments = true;
// Add leading comments to current group
while (j >= 0 && stillInComments) {
const priorCstNode = expsCsts[j];
if (isOnlyComment(priorCstNode)) {
currCstGroup.push(priorCstNode);
j--;
i--;
} else {
stillInComments = false;
}
}
// Reverse since we scanned backwards
currCstGroup.reverse();
cstGroups.push(currCstGroup);
currCstGroup = [];
}
}
if (currCstGroup.length > 0) {
currCstGroup.reverse();
cstGroups.push(currCstGroup);
}
// Adjust for reverse scanning
cstGroups.reverse();
const docGroups = cstGroups.map((currGroup) => this.mapVisit(currGroup));
// Add newlines between group elements
const docGroupsInnerNewlines = docGroups.map((currGroup) =>
join(line, currGroup)
);
const docGroupsOuterNewlines = join([line, line], docGroupsInnerNewlines);
return [docGroupsOuterNewlines, line];
}
/**
* Visit an expression (keyval, table, or comment)
*/
expression(ctx: TomlExpression): Doc | string {
if (ctx.keyval) {
let keyValDoc = this.visit(ctx.keyval[0]);
if (ctx.Comment) {
const commentText = trimComment(ctx.Comment[0].image);
keyValDoc = [keyValDoc, ' ' + commentText];
}
return keyValDoc;
} else if (ctx.table) {
let tableDoc = this.visit(ctx.table[0]);
if (ctx.Comment) {
const commentText = trimComment(ctx.Comment[0].image);
tableDoc = [tableDoc, ' ' + commentText];
}
return tableDoc;
} else if (ctx.Comment) {
return trimComment(ctx.Comment[0].image);
}
return '';
}
/**
* Visit a key-value pair
*/
keyval(ctx: TomlKeyVal): Doc {
const keyDoc = this.visit(ctx.key[0]);
const valueDoc = this.visit(ctx.val[0]);
return [keyDoc, ' = ', valueDoc];
}
/**
* Visit a key
*/
key(ctx: any): Doc {
const keyTexts = ctx.IKey?.map((tok: any) => {
const keyText = tok.image;
// Apply key formatting (add/remove quotes as needed)
return formatKey(keyText);
}) || [];
return join('.', keyTexts);
}
/**
* Visit a value
*/
val(ctx: any): Doc | string {
try {
const actualValueNode = getSingle(ctx);
if (actualValueNode.image !== undefined) {
// Terminal token - 优化值的表示
return optimizeValue(actualValueNode.image);
} else {
return this.visit(actualValueNode);
}
} catch (error) {
// 如果getSingle失败尝试直接处理children
if (ctx.children) {
// 处理不同类型的值
for (const [childKey, childNodes] of Object.entries(ctx.children)) {
if (Array.isArray(childNodes) && childNodes.length > 0) {
const firstChild = childNodes[0];
// 处理基本类型
if (firstChild.image !== undefined) {
// 优化值的表示(特别是字符串)
return optimizeValue(firstChild.image);
}
// 处理复杂类型(如数组、内联表等)
if (firstChild.name) {
return this.visit(firstChild);
}
}
}
}
return '';
}
}
/**
* Visit an array
*/
array(ctx: any): Doc {
const arrayValuesDocs = ctx.arrayValues ? this.visit(ctx.arrayValues) : '';
const postComments = collectComments(ctx.commentNewline);
const commentsDocs = postComments.map((commentTok) => {
const trimmedCommentText = trimComment(commentTok.image);
return [hardline, trimmedCommentText];
});
return group(['[', indent([arrayValuesDocs, commentsDocs]), softline, ']']);
}
/**
* Visit array values
*/
arrayValues(ctx: any): Doc {
const values = ctx.val || [];
const commas = ctx.Comma || [];
const comments = collectComments(ctx.commentNewline);
const itemsCst = [...values, ...commas, ...comments];
itemsCst.sort((a, b) => {
const aOffset = arrItemOffset(a);
const bOffset = arrItemOffset(b);
return aOffset - bOffset;
});
const itemsDoc: Doc[] = [];
for (let i = 0; i < itemsCst.length; i++) {
const cstItem = itemsCst[i];
if (cstItem.name === 'val') {
const valDoc = this.visit(cstItem);
const valEndLine = arrItemProp(cstItem, 'endLine');
let potentialComma = '';
// Handle next item (comma or comment)
if (itemsCst[i + 1]) {
let nextPossibleComment = itemsCst[i + 1];
// Skip commas
if (nextPossibleComment.tokenType === t.Comma) {
potentialComma = ',';
i++;
nextPossibleComment = itemsCst[i + 1];
}
// Handle same-line comments
if (
nextPossibleComment &&
nextPossibleComment.tokenType === t.Comment &&
nextPossibleComment.startLine === valEndLine
) {
i++;
const trimmedComment = trimComment(nextPossibleComment.image);
const comment = ' ' + trimmedComment;
itemsDoc.push([valDoc, potentialComma, comment, hardline]);
} else {
// No comment on same line
const isTrailingComma = i === itemsCst.length - 1;
const optionalCommaLineBreak = isTrailingComma
? ifBreak(',', '') // Only print trailing comma if multiline array
: [potentialComma, line];
itemsDoc.push([valDoc, optionalCommaLineBreak]);
}
} else {
// Last item without followup
itemsDoc.push([valDoc]);
}
} else if (cstItem.tokenType === t.Comment) {
// Separate line comment
const trimmedComment = trimComment(cstItem.image);
itemsDoc.push([trimmedComment, hardline]);
} else {
throw new Error('Non-exhaustive match in arrayValues');
}
}
return [softline, itemsDoc];
}
/**
* Visit an inline table
*/
inlineTable(ctx: any): Doc {
const inlineTableKeyValsDocs = ctx.inlineTableKeyVals
? this.visit(ctx.inlineTableKeyVals)
: '';
return group(['{ ', inlineTableKeyValsDocs, ' }']);
}
/**
* Visit inline table key-value pairs
*/
inlineTableKeyVals(ctx: any): Doc {
const keyValDocs = this.mapVisit(ctx.keyval);
return join(', ', keyValDocs);
}
/**
* Visit a table
*/
table(ctx: any): Doc | string {
return this.visitSingle(ctx);
}
/**
* Visit a standard table
*/
stdTable(ctx: any): Doc {
if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) {
const keyTexts = ctx.key[0].children.IKey.map((tok: any) => {
return formatKey(tok.image);
});
return ['[', join('.', keyTexts), ']'];
}
return '[]';
}
/**
* Visit an array table
*/
arrayTable(ctx: any): Doc {
if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) {
const keyTexts = ctx.key[0].children.IKey.map((tok: any) => {
return formatKey(tok.image);
});
return ['[[', join('.', keyTexts), ']]'];
}
return '[[]]';
}
/**
* Visit newline (should not be called)
*/
nl(ctx: any): never {
throw new Error('Should not get here!');
}
/**
* Visit comment newline (no-op)
*/
commentNewline(ctx: any): void {
// No operation needed
}
}
// Create singleton visitor instance
const beautifierVisitor = new TomlBeautifierVisitor();
/**
* Main print function for Prettier
* @param path - AST path from Prettier
* @param options - Print options
* @param print - Print function (unused in this implementation)
* @returns Formatted document
*/
export function print(path: AstPath<TomlCstNode>, options?: any, print?: any): Doc {
const cst = path.node as TomlDocument;
return beautifierVisitor.visit(cst);
}

View File

@@ -0,0 +1,62 @@
/**
* TypeScript type definitions for TOML Prettier plugin
*/
// TOML CST Node types based on @toml-tools/parser
export interface TomlCstNode {
name?: string;
image?: string;
children?: Record<string, TomlCstNode[]>;
startOffset?: number;
endOffset?: number;
startLine?: number;
endLine?: number;
tokenType?: any;
}
export interface TomlComment extends TomlCstNode {
image: string;
}
export interface TomlContext {
[key: string]: TomlCstNode[];
}
export interface TomlValue extends TomlCstNode {
children: TomlContext;
}
export interface TomlKeyVal extends TomlCstNode {
key: TomlCstNode[];
val: TomlCstNode[];
}
export interface TomlArray extends TomlCstNode {
arrayValues?: TomlCstNode;
commentNewline?: TomlCstNode[];
}
export interface TomlInlineTable extends TomlCstNode {
inlineTableKeyVals?: TomlCstNode;
}
export interface TomlTable extends TomlCstNode {
table: TomlCstNode[];
}
export interface TomlExpression extends TomlCstNode {
keyval?: TomlKeyVal[];
table?: TomlTable[];
Comment?: TomlComment[];
}
export interface TomlDocument extends TomlCstNode {
expression?: TomlExpression[];
}
// Print options for TOML formatting
export interface TomlPrintOptions {
printWidth?: number;
tabWidth?: number;
useTabs?: boolean;
}

View File

@@ -29,6 +29,10 @@ export const useThemeStore = defineStore('theme', () => {
const initializeThemeColors = async () => {
try {
const themes = await ThemeService.GetDefaultThemes();
if (!themes) {
Object.assign(themeColors.darkTheme, defaultDarkColors);
Object.assign(themeColors.lightTheme, defaultLightColors);
}
if (themes[ThemeType.ThemeTypeDark]) {
Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors);
}

View File

@@ -36,14 +36,18 @@ import htmlPrettierPlugin from "prettier/plugins/html"
import cssPrettierPlugin from "prettier/plugins/postcss"
import markdownPrettierPlugin from "prettier/plugins/markdown"
import yamlPrettierPlugin from "prettier/plugins/yaml"
import goPrettierPlugin from "@/common/prettier/plugins/go/go"
import goPrettierPlugin from "@/common/prettier/plugins/go/go.mjs"
import sqlPrettierPlugin from "@/common/prettier/plugins/sql/sql"
import phpPrettierPlugin from "@/common/prettier/plugins/php"
import javaPrettierPlugin from "@/common/prettier/plugins/java"
import xmlPrettierPlugin from "@prettier/plugin-xml"
import * as rustPrettierPlugin from "@/common/prettier/plugins/rust";
import * as shellPrettierPlugin from "@/common/prettier/plugins/shell";
import tomlPrettierPlugin from "prettier-plugin-toml";
import tomlPrettierPlugin from "@/common/prettier/plugins/toml";
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
import powershellPrettierPlugin from "@/common/prettier/plugins/powershell";
import scalaPrettierPlugin from "@/common/prettier/plugins/scala";
import * as prettierPluginEstree from "prettier/plugins/estree";
/**
@@ -119,10 +123,13 @@ export const LANGUAGES: LanguageInfo[] = [
plugins: [tomlPrettierPlugin]
}),
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser, {
parser: "go",
parser: "go-format",
plugins: [goPrettierPlugin]
}),
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser,{
parser: "clojure",
plugins: [clojurePrettierPlugin]
}),
new LanguageInfo("ex", "Elixir", elixir().language.parser),
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser, {
@@ -135,10 +142,19 @@ export const LANGUAGES: LanguageInfo[] = [
}),
new LanguageInfo("swift", "Swift", StreamLanguage.define(swift).parser),
new LanguageInfo("kt", "Kotlin", StreamLanguage.define(kotlin).parser),
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser,{
parser: "groovy",
plugins: [groovyPrettierPlugin]
}),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser,{
parser: "powershell",
plugins: [powershellPrettierPlugin]
}),
new LanguageInfo("dart", "Dart", null), // 暂无解析器
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser),
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser,{
parser: "scala",
plugins: [scalaPrettierPlugin]
}),
];
/**

View File

@@ -72,6 +72,66 @@
</div>
</SettingSection>
<!-- Go代码格式化测试区域 -->
<SettingSection title="Go Code Formatter Test">
<SettingItem title="Go Code Input">
<textarea
v-model="goCode"
placeholder="Enter Go code to format..."
class="select-input code-textarea"
rows="8"
></textarea>
</SettingItem>
<SettingItem title="Actions">
<div class="button-group">
<button @click="testGoFormatter" class="test-button primary" :disabled="isFormatting">
{{ isFormatting ? 'Formatting...' : 'Format Go Code' }}
</button>
<button @click="resetGoCode" class="test-button">
Reset to Sample
</button>
<button @click="loadComplexSample" class="test-button">
Load Complex Sample
</button>
<button @click="loadBrokenSample" class="test-button">
Load Broken Sample
</button>
<button @click="checkWasmStatus" class="test-button">
Check WASM Status
</button>
<button @click="initializeGoWasm" class="test-button" :disabled="isInitializing">
{{ isInitializing ? 'Initializing...' : 'Initialize Go WASM' }}
</button>
</div>
</SettingItem>
<!-- 加载状态和进度 -->
<div v-if="formatStatus" class="test-status detailed-status">
<div class="status-header" :class="formatStatus.type">
<strong>{{ formatStatus.type.toUpperCase() }}:</strong> {{ formatStatus.message }}
</div>
<div v-if="formatStatus.details" class="status-details">
<div v-for="(detail, index) in formatStatus.details" :key="index" class="status-detail">
<span class="detail-time">[{{ detail.time }}]</span>
<span class="detail-message">{{ detail.message }}</span>
</div>
</div>
<div v-if="formatStatus.duration" class="status-duration">
执行时间: {{ formatStatus.duration }}ms
</div>
</div>
<!-- 格式化结果 -->
<SettingItem v-if="formattedCode" title="Formatted Result">
<textarea
v-model="formattedCode"
readonly
class="select-input code-textarea result-textarea"
rows="8"
></textarea>
</SettingItem>
</SettingSection>
<!-- 清除所有测试状态 -->
<SettingSection title="Cleanup">
<SettingItem title="Clear All">
@@ -91,6 +151,8 @@ import { ref } from 'vue'
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
import SettingSection from '../components/SettingSection.vue'
import SettingItem from '../components/SettingItem.vue'
import { format } from 'prettier'
import goPrettierPlugin from '@/common/prettier/plugins/go/go.mjs'
// Badge测试状态
const badgeText = ref('')
@@ -105,6 +167,33 @@ const notificationStatus = ref<{ type: string; message: string } | null>(null)
// 清除状态
const clearStatus = ref<{ type: string; message: string } | null>(null)
// Go代码格式化测试状态
const goCode = ref(`package main
import(
"fmt"
"os"
)
func main(){
if len(os.Args)<2{
fmt.Println("Usage: program <name>")
return
}
name:=os.Args[1]
fmt.Printf("Hello, %s!\\n",name)
}`)
const formattedCode = ref('')
const isFormatting = ref(false)
const isInitializing = ref(false)
const formatStatus = ref<{
type: 'success' | 'error' | 'info' | 'warning';
message: string;
details?: Array<{ time: string; message: string }>;
duration?: number;
} | null>(null)
// 显示状态消息的辅助函数
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
statusRef.value = { type, message }
@@ -158,6 +247,382 @@ const testUpdateNotification = async () => {
}
}
// Go代码格式化相关函数
const addFormatDetail = (message: string) => {
const time = new Date().toLocaleTimeString()
if (!formatStatus.value) {
formatStatus.value = {
type: 'info',
message: '正在执行...',
details: []
}
}
if (!formatStatus.value.details) {
formatStatus.value.details = []
}
formatStatus.value.details.push({ time, message })
}
// 检查WASM状态
const checkWasmStatus = async () => {
formatStatus.value = {
type: 'info',
message: '检查WASM状态...',
details: []
}
addFormatDetail('开始检查环境...')
try {
// 检查浏览器环境
addFormatDetail('检查浏览器环境支持')
if (typeof WebAssembly === 'undefined') {
throw new Error('WebAssembly not supported in this browser')
}
addFormatDetail('✅ WebAssembly 支持正常')
// 检查Go运行时
addFormatDetail('检查Go运行时状态')
if (typeof globalThis.Go !== 'undefined') {
addFormatDetail('✅ Go运行时已加载')
} else {
addFormatDetail('❌ Go运行时未加载')
}
// 检查formatGo函数
addFormatDetail('检查formatGo函数')
if (typeof globalThis.formatGo === 'function') {
addFormatDetail('✅ formatGo函数可用')
} else {
addFormatDetail('❌ formatGo函数不可用')
}
// 检查WASM文件可访问性
addFormatDetail('检查WASM文件可访问性')
try {
const response = await fetch('/go.wasm', { method: 'HEAD' })
if (response.ok) {
addFormatDetail('✅ go.wasm文件可访问')
} else {
addFormatDetail(`❌ go.wasm文件不可访问: ${response.status}`)
}
} catch (error) {
addFormatDetail(`❌ go.wasm文件访问失败: ${error}`)
}
// 检查wasm_exec.js
addFormatDetail('检查wasm_exec.js文件')
try {
const response = await fetch('/wasm_exec.js', { method: 'HEAD' })
if (response.ok) {
addFormatDetail('✅ wasm_exec.js文件可访问')
} else {
addFormatDetail(`❌ wasm_exec.js文件不可访问: ${response.status}`)
}
} catch (error) {
addFormatDetail(`❌ wasm_exec.js文件访问失败: ${error}`)
}
formatStatus.value.type = 'success'
formatStatus.value.message = 'WASM状态检查完成'
} catch (error: any) {
addFormatDetail(`❌ 检查失败: ${error.message}`)
formatStatus.value.type = 'error'
formatStatus.value.message = `WASM状态检查失败: ${error.message}`
}
}
// 手动初始化 Go WASM
const initializeGoWasm = async () => {
if (isInitializing.value) return
isInitializing.value = true
formatStatus.value = {
type: 'info',
message: '正在初始化 Go WASM...',
details: []
}
try {
addFormatDetail('开始手动初始化 Go WASM')
// 直接调用插件的初始化函数
const { initialize } = await import('@/common/prettier/plugins/go/go.mjs')
addFormatDetail('调用插件初始化函数...')
await initialize()
addFormatDetail('检查 formatGo 函数是否可用...')
if (typeof globalThis.formatGo === 'function') {
addFormatDetail('✅ formatGo 函数初始化成功')
// 测试函数
addFormatDetail('测试 formatGo 函数...')
const testCode = 'package main\nfunc main(){}'
const result = globalThis.formatGo(testCode)
addFormatDetail(`✅ 测试成功,格式化后长度: ${result.length}`)
formatStatus.value = {
type: 'success',
message: 'Go WASM 初始化成功!',
details: formatStatus.value.details
}
} else {
throw new Error('formatGo 函数仍然不可用')
}
} catch (error: any) {
addFormatDetail(`❌ 初始化失败: ${error.message}`)
formatStatus.value = {
type: 'error',
message: `Go WASM 初始化失败: ${error.message}`,
details: formatStatus.value.details
}
} finally {
isInitializing.value = false
}
}
// 测试Go代码格式化
const testGoFormatter = async () => {
if (isFormatting.value) return
isFormatting.value = true
formattedCode.value = ''
const startTime = Date.now()
formatStatus.value = {
type: 'info',
message: '正在格式化Go代码...',
details: []
}
try {
addFormatDetail('开始格式化流程')
addFormatDetail(`输入代码长度: ${goCode.value.length} 字符`)
// 设置超时检测
const timeoutId = setTimeout(() => {
addFormatDetail('⚠️ 格式化超时 (10秒),可能存在阻塞')
}, 10000)
addFormatDetail('调用prettier格式化...')
const result = await format(goCode.value, {
parser: 'go-format',
plugins: [goPrettierPlugin]
})
clearTimeout(timeoutId)
const duration = Date.now() - startTime
addFormatDetail('✅ 格式化完成')
addFormatDetail(`输出代码长度: ${result.length} 字符`)
formattedCode.value = result
formatStatus.value = {
type: 'success',
message: '代码格式化成功!',
details: formatStatus.value.details,
duration
}
} catch (error: any) {
const duration = Date.now() - startTime
addFormatDetail(`❌ 格式化失败: ${error.message}`)
// 详细错误分析
if (error.message.includes('WASM')) {
addFormatDetail('可能原因: WASM模块加载或初始化问题')
} else if (error.message.includes('formatGo')) {
addFormatDetail('可能原因: Go函数未正确暴露到全局作用域')
} else if (error.message.includes('timeout')) {
addFormatDetail('可能原因: 代码执行超时或阻塞')
}
formatStatus.value = {
type: 'error',
message: `格式化失败: ${error.message}`,
details: formatStatus.value.details,
duration
}
} finally {
isFormatting.value = false
}
}
// 重置Go代码为示例
const resetGoCode = () => {
goCode.value = `package main
import(
"fmt"
"os"
)
func main(){
if len(os.Args)<2{
fmt.Println("Usage: program <name>")
return
}
name:=os.Args[1]
fmt.Printf("Hello, %s!\\n",name)
}`
formattedCode.value = ''
formatStatus.value = null
}
// 加载复杂示例
const loadComplexSample = () => {
goCode.value = `package main
import(
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type User struct{
ID int \`json:"id"\`
Name string \`json:"name"\`
Email string \`json:"email"\`
CreatedAt time.Time \`json:"created_at"\`
}
type UserService struct{
users []User
nextID int
}
func NewUserService()*UserService{
return &UserService{
users:make([]User,0),
nextID:1,
}
}
func(s *UserService)CreateUser(name,email string)*User{
user:=User{
ID:s.nextID,
Name:name,
Email:email,
CreatedAt:time.Now(),
}
s.users=append(s.users,user)
s.nextID++
return &user
}
func(s *UserService)GetUser(id int)*User{
for i:=range s.users{
if s.users[i].ID==id{
return &s.users[i]
}
}
return nil
}
func(s *UserService)ListUsers()[]User{
return s.users
}
func main(){
service:=NewUserService()
http.HandleFunc("/users",func(w http.ResponseWriter,r *http.Request){
switch r.Method{
case http.MethodGet:
users:=service.ListUsers()
w.Header().Set("Content-Type","application/json")
json.NewEncoder(w).Encode(users)
case http.MethodPost:
body,err:=ioutil.ReadAll(r.Body)
if err!=nil{
http.Error(w,"Bad request",http.StatusBadRequest)
return
}
var req struct{
Name string \`json:"name"\`
Email string \`json:"email"\`
}
if err:=json.Unmarshal(body,&req);err!=nil{
http.Error(w,"Invalid JSON",http.StatusBadRequest)
return
}
user:=service.CreateUser(req.Name,req.Email)
w.Header().Set("Content-Type","application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
default:
http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/users/",func(w http.ResponseWriter,r *http.Request){
if r.Method!=http.MethodGet{
http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)
return
}
idStr:=strings.TrimPrefix(r.URL.Path,"/users/")
id,err:=strconv.Atoi(idStr)
if err!=nil{
http.Error(w,"Invalid user ID",http.StatusBadRequest)
return
}
user:=service.GetUser(id)
if user==nil{
http.Error(w,"User not found",http.StatusNotFound)
return
}
w.Header().Set("Content-Type","application/json")
json.NewEncoder(w).Encode(user)
})
port:=os.Getenv("PORT")
if port==""{
port="8080"
}
fmt.Printf("Server starting on port %s\\n",port)
log.Fatal(http.ListenAndServe(":"+port,nil))
}`
formattedCode.value = ''
formatStatus.value = null
}
// 加载有语法错误的示例
const loadBrokenSample = () => {
goCode.value = `package main
import(
"fmt"
"os
)
func main({
if len(os.Args<2{
fmt.Println("Usage: program <name>")
return
}
name:=os.Args[1
fmt.Printf("Hello, %s!\\n",name)
`
formattedCode.value = ''
formatStatus.value = null
}
// 清除所有测试状态
const clearAll = async () => {
try {
@@ -167,6 +632,10 @@ const clearAll = async () => {
notificationTitle.value = ''
notificationSubtitle.value = ''
notificationBody.value = ''
// 清空Go测试状态
formattedCode.value = ''
formatStatus.value = null
resetGoCode()
showStatus(clearStatus, 'success', 'All test states cleared successfully')
} catch (error: any) {
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
@@ -207,6 +676,25 @@ const clearAll = async () => {
font-family: inherit;
line-height: 1.4;
}
&.code-textarea {
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 11px;
line-height: 1.5;
width: 100%;
max-width: 600px;
min-height: 120px;
white-space: pre;
overflow-wrap: normal;
word-break: normal;
tab-size: 2;
&.result-textarea {
background-color: var(--settings-card-bg);
border-color: #22c55e;
color: var(--settings-text);
}
}
}
.button-group {
@@ -271,4 +759,47 @@ const clearAll = async () => {
border-color: rgba(239, 68, 68, 0.2);
}
}
.detailed-status {
.status-header {
margin-bottom: 8px;
font-weight: 600;
}
.status-details {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
padding: 8px;
margin: 8px 0;
max-height: 200px;
overflow-y: auto;
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
font-size: 10px;
line-height: 1.4;
.status-detail {
margin-bottom: 2px;
display: flex;
gap: 8px;
.detail-time {
color: var(--settings-text-secondary);
flex-shrink: 0;
font-weight: 500;
}
.detail-message {
color: var(--settings-text);
word-break: break-word;
}
}
}
.status-duration {
margin-top: 8px;
font-size: 10px;
color: var(--settings-text-secondary);
font-weight: 500;
}
}
</style>