Compare commits
2 Commits
42c7d11c09
...
338ac358db
| Author | SHA1 | Date | |
|---|---|---|---|
| 338ac358db | |||
| a83c7139c9 |
123
frontend/package-lock.json
generated
123
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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 {
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = '/wasm_exec.js';
|
||||
document.head.appendChild(script);
|
||||
|
||||
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);
|
||||
});
|
||||
// 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 __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Load wasm_exec.js
|
||||
const wasmExecPath = path.join(__dirname, 'wasm_exec.js');
|
||||
require(wasmExecPath);
|
||||
|
||||
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();
|
||||
|
||||
const go = new globalThis.Go();
|
||||
const wasmBuffer = await loadWasm();
|
||||
const {instance} = await WebAssembly.instantiate(wasmBuffer, go.importObject);
|
||||
|
||||
// Run Go program
|
||||
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');
|
||||
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();
|
||||
|
||||
// 详细检查 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);
|
||||
});
|
||||
} 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,
|
||||
|
||||
BIN
frontend/src/common/prettier/plugins/go/go.wasm
Normal file
BIN
frontend/src/common/prettier/plugins/go/go.wasm
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
561
frontend/src/common/prettier/plugins/go/wasm_exec.js
Normal file
561
frontend/src/common/prettier/plugins/go/wasm_exec.js
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
67
frontend/src/common/prettier/plugins/groovy/index.ts
Normal file
67
frontend/src/common/prettier/plugins/groovy/index.ts
Normal 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;
|
||||
391
frontend/src/common/prettier/plugins/powershell/ast.ts
Normal file
391
frontend/src/common/prettier/plugins/powershell/ast.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
208
frontend/src/common/prettier/plugins/powershell/index.ts
Normal file
208
frontend/src/common/prettier/plugins/powershell/index.ts
Normal 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;
|
||||
722
frontend/src/common/prettier/plugins/powershell/lexer.ts
Normal file
722
frontend/src/common/prettier/plugins/powershell/lexer.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
821
frontend/src/common/prettier/plugins/powershell/parser.ts
Normal file
821
frontend/src/common/prettier/plugins/powershell/parser.ts
Normal 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)) {
|
||||
// 继续跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
109
frontend/src/common/prettier/plugins/scala/index.ts
Normal file
109
frontend/src/common/prettier/plugins/scala/index.ts
Normal 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;
|
||||
91
frontend/src/common/prettier/plugins/scala/printer.ts
Normal file
91
frontend/src/common/prettier/plugins/scala/printer.ts
Normal 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";
|
||||
},
|
||||
};
|
||||
}
|
||||
206
frontend/src/common/prettier/plugins/scala/scala-parser/index.ts
Normal file
206
frontend/src/common/prettier/plugins/scala/scala-parser/index.ts
Normal 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.
|
||||
479
frontend/src/common/prettier/plugins/scala/scala-parser/lexer.ts
Normal file
479
frontend/src/common/prettier/plugins/scala/scala-parser/lexer.ts
Normal 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;
|
||||
1929
frontend/src/common/prettier/plugins/scala/scala-parser/parser.ts
Normal file
1929
frontend/src/common/prettier/plugins/scala/scala-parser/parser.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 };
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
538
frontend/src/common/prettier/plugins/scala/visitor.ts
Normal file
538
frontend/src/common/prettier/plugins/scala/visitor.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
433
frontend/src/common/prettier/plugins/scala/visitor/scala3.ts
Normal file
433
frontend/src/common/prettier/plugins/scala/visitor/scala3.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
658
frontend/src/common/prettier/plugins/scala/visitor/statements.ts
Normal file
658
frontend/src/common/prettier/plugins/scala/visitor/statements.ts
Normal 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 "";
|
||||
}
|
||||
}
|
||||
474
frontend/src/common/prettier/plugins/scala/visitor/types.ts
Normal file
474
frontend/src/common/prettier/plugins/scala/visitor/types.ts
Normal 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 "";
|
||||
}
|
||||
}
|
||||
295
frontend/src/common/prettier/plugins/scala/visitor/utils.ts
Normal file
295
frontend/src/common/prettier/plugins/scala/visitor/utils.ts
Normal 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;
|
||||
}
|
||||
72
frontend/src/common/prettier/plugins/toml/index.ts
Normal file
72
frontend/src/common/prettier/plugins/toml/index.ts
Normal 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;
|
||||
82
frontend/src/common/prettier/plugins/toml/loc.ts
Normal file
82
frontend/src/common/prettier/plugins/toml/loc.ts
Normal 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;
|
||||
}
|
||||
284
frontend/src/common/prettier/plugins/toml/printer-utils.ts
Normal file
284
frontend/src/common/prettier/plugins/toml/printer-utils.ts
Normal 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}`);
|
||||
}
|
||||
413
frontend/src/common/prettier/plugins/toml/printer.ts
Normal file
413
frontend/src/common/prettier/plugins/toml/printer.ts
Normal 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);
|
||||
}
|
||||
62
frontend/src/common/prettier/plugins/toml/types.ts
Normal file
62
frontend/src/common/prettier/plugins/toml/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user