Compare commits
3 Commits
c26c11e253
...
f72010bd69
| Author | SHA1 | Date | |
|---|---|---|---|
| f72010bd69 | |||
| cd027097f8 | |||
| 9cbbf729c0 |
BIN
frontend/public/go-format.wasm
Normal file
BIN
frontend/public/go-format.wasm
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,561 +0,0 @@
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
117
frontend/src/common/prettier/plugins/clang/CustomFileSystem.cc
Normal file
117
frontend/src/common/prettier/plugins/clang/CustomFileSystem.cc
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "CustomFileSystem.h"
|
||||
#include "llvm/ADT/StringExtras.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include <emscripten.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
using namespace llvm;
|
||||
using namespace llvm::vfs;
|
||||
|
||||
namespace {
|
||||
|
||||
bool isRunningOnWindows() {
|
||||
return EM_ASM_INT({return process.platform == 'win32' ? 1 : 0}) == 1;
|
||||
}
|
||||
|
||||
std::error_code current_path(SmallVectorImpl<char> &result) {
|
||||
result.clear();
|
||||
|
||||
const char *pwd = ::getenv("PWD");
|
||||
result.append(pwd, pwd + strlen(pwd));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace llvm {
|
||||
namespace vfs {
|
||||
|
||||
sys::path::Style getPathStyle() {
|
||||
static sys::path::Style cachedStyle = sys::path::Style::native;
|
||||
|
||||
if (cachedStyle == sys::path::Style::native) {
|
||||
cachedStyle = isRunningOnWindows() ? sys::path::Style::windows
|
||||
: sys::path::Style::posix;
|
||||
}
|
||||
|
||||
return cachedStyle;
|
||||
}
|
||||
|
||||
void make_absolute(const Twine ¤t_directory,
|
||||
SmallVectorImpl<char> &path) {
|
||||
StringRef p(path.data(), path.size());
|
||||
|
||||
auto pathStyle = getPathStyle();
|
||||
|
||||
bool rootDirectory = sys::path::has_root_directory(p, pathStyle);
|
||||
bool rootName = sys::path::has_root_name(p, pathStyle);
|
||||
|
||||
// Already absolute.
|
||||
if ((rootName || is_style_posix(pathStyle)) && rootDirectory)
|
||||
return;
|
||||
|
||||
// All of the following conditions will need the current directory.
|
||||
SmallString<128> current_dir;
|
||||
current_directory.toVector(current_dir);
|
||||
|
||||
// Relative path. Prepend the current directory.
|
||||
if (!rootName && !rootDirectory) {
|
||||
// Append path to the current directory.
|
||||
sys::path::append(current_dir, pathStyle, p);
|
||||
// Set path to the result.
|
||||
path.swap(current_dir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rootName && rootDirectory) {
|
||||
StringRef cdrn = sys::path::root_name(current_dir, pathStyle);
|
||||
SmallString<128> curDirRootName(cdrn.begin(), cdrn.end());
|
||||
sys::path::append(curDirRootName, pathStyle, p);
|
||||
// Set path to the result.
|
||||
path.swap(curDirRootName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootName && !rootDirectory) {
|
||||
StringRef pRootName = sys::path::root_name(p, pathStyle);
|
||||
StringRef bRootDirectory =
|
||||
sys::path::root_directory(current_dir, pathStyle);
|
||||
StringRef bRelativePath = sys::path::relative_path(current_dir, pathStyle);
|
||||
StringRef pRelativePath = sys::path::relative_path(p, pathStyle);
|
||||
|
||||
SmallString<128> res;
|
||||
sys::path::append(res, pathStyle, pRootName, bRootDirectory, bRelativePath,
|
||||
pRelativePath);
|
||||
path.swap(res);
|
||||
return;
|
||||
}
|
||||
|
||||
llvm_unreachable("All rootName and rootDirectory combinations should have "
|
||||
"occurred above!");
|
||||
}
|
||||
|
||||
std::error_code make_absolute(SmallVectorImpl<char> &path) {
|
||||
if (sys::path::is_absolute(path, getPathStyle()))
|
||||
return {};
|
||||
|
||||
SmallString<128> current_dir;
|
||||
if (std::error_code ec = current_path(current_dir))
|
||||
return ec;
|
||||
|
||||
make_absolute(current_dir, path);
|
||||
return {};
|
||||
}
|
||||
|
||||
CustomFileSystem::CustomFileSystem(IntrusiveRefCntPtr<FileSystem> FS)
|
||||
: ProxyFileSystem(std::move(FS)) {}
|
||||
|
||||
std::error_code
|
||||
CustomFileSystem::makeAbsolute(SmallVectorImpl<char> &Path) const {
|
||||
return make_absolute(Path);
|
||||
}
|
||||
|
||||
} // namespace vfs
|
||||
} // namespace llvm
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef CUSTOM_FILE_SYSTEM_H
|
||||
#define CUSTOM_FILE_SYSTEM_H
|
||||
|
||||
#include "llvm/ADT/IntrusiveRefCntPtr.h"
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/Support/ErrorOr.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/VirtualFileSystem.h"
|
||||
|
||||
namespace llvm {
|
||||
namespace vfs {
|
||||
|
||||
sys::path::Style getPathStyle();
|
||||
std::error_code make_absolute(SmallVectorImpl<char> &path);
|
||||
|
||||
class CustomFileSystem : public ProxyFileSystem {
|
||||
public:
|
||||
CustomFileSystem(IntrusiveRefCntPtr<FileSystem> FS);
|
||||
|
||||
std::error_code makeAbsolute(SmallVectorImpl<char> &Path) const override;
|
||||
};
|
||||
|
||||
} // namespace vfs
|
||||
} // namespace llvm
|
||||
|
||||
#endif // CUSTOM_FILE_SYSTEM_H
|
||||
26
frontend/src/common/prettier/plugins/clang/binding.cc
Normal file
26
frontend/src/common/prettier/plugins/clang/binding.cc
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "lib.h"
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
register_vector<unsigned>("RangeList");
|
||||
|
||||
value_object<Result>("Result")
|
||||
.field("error", &Result::error)
|
||||
.field("content", &Result::content);
|
||||
|
||||
function<std::string>("version", &version);
|
||||
function<Result, const std::string, const std::string, const std::string>(
|
||||
"format", &format);
|
||||
function<Result, const std::string, const std::string, const std::string,
|
||||
const std::vector<unsigned>>("format_byte", &format_byte);
|
||||
function<Result, const std::string, const std::string, const std::string,
|
||||
const std::vector<unsigned>>("format_line", &format_line);
|
||||
function<void, const std::string>("set_fallback_style", &set_fallback_style);
|
||||
function<void, bool>("set_sort_includes", &set_sort_includes);
|
||||
function<Result, const std::string, const std::string, const std::string>(
|
||||
"dump_config", &dump_config);
|
||||
}
|
||||
|
||||
int main(void) {}
|
||||
File diff suppressed because one or more lines are too long
BIN
frontend/src/common/prettier/plugins/clang/clang-format-cli.wasm
Normal file
BIN
frontend/src/common/prettier/plugins/clang/clang-format-cli.wasm
Normal file
Binary file not shown.
197
frontend/src/common/prettier/plugins/clang/clang-format-diff.py
Normal file
197
frontend/src/common/prettier/plugins/clang/clang-format-diff.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# ===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===#
|
||||
#
|
||||
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
# See https://llvm.org/LICENSE.txt for license information.
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
#
|
||||
# ===------------------------------------------------------------------------===#
|
||||
|
||||
"""
|
||||
This script reads input from a unified diff and reformats all the changed
|
||||
lines. This is useful to reformat all the lines touched by a specific patch.
|
||||
Example usage for git/svn users:
|
||||
|
||||
git diff -U0 --no-color --relative HEAD^ | {clang_format_diff} -p1 -i
|
||||
svn diff --diff-cmd=diff -x-U0 | {clang_format_diff} -i
|
||||
|
||||
It should be noted that the filename contained in the diff is used unmodified
|
||||
to determine the source file to update. Users calling this script directly
|
||||
should be careful to ensure that the path in the diff is correct relative to the
|
||||
current working directory.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if sys.version_info.major >= 3:
|
||||
from io import StringIO
|
||||
else:
|
||||
from io import BytesIO as StringIO
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__.format(clang_format_diff="%(prog)s"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="apply edits to files instead of displaying a diff",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
metavar="NUM",
|
||||
default=0,
|
||||
help="strip the smallest prefix containing P slashes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-regex",
|
||||
metavar="PATTERN",
|
||||
default=None,
|
||||
help="custom pattern selecting file paths to reformat "
|
||||
"(case sensitive, overrides -iregex)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-iregex",
|
||||
metavar="PATTERN",
|
||||
default=r".*\.(?:cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp"
|
||||
r"|hxx|m|mm|inc|js|ts|proto|protodevel|java|cs|json|ipynb|s?vh?)",
|
||||
help="custom pattern selecting file paths to reformat "
|
||||
"(case insensitive, overridden by -regex)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-sort-includes",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="let clang-format sort include blocks",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="be more verbose, ineffective without -i",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-style",
|
||||
help="formatting style to apply (LLVM, GNU, Google, Chromium, "
|
||||
"Microsoft, Mozilla, WebKit)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-fallback-style",
|
||||
help="The name of the predefined style used as a"
|
||||
"fallback in case clang-format is invoked with"
|
||||
"-style=file, but can not find the .clang-format"
|
||||
"file to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-binary",
|
||||
default="clang-format",
|
||||
help="location of binary to use for clang-format",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Extract changed lines for each file.
|
||||
filename = None
|
||||
lines_by_file = {}
|
||||
for line in sys.stdin:
|
||||
match = re.search(r"^\+\+\+\ (.*?/){%s}(.+)" % args.p, line.rstrip())
|
||||
if match:
|
||||
filename = match.group(2)
|
||||
if filename is None:
|
||||
continue
|
||||
|
||||
if args.regex is not None:
|
||||
if not re.match("^%s$" % args.regex, filename):
|
||||
continue
|
||||
else:
|
||||
if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
|
||||
continue
|
||||
|
||||
match = re.search(r"^@@.*\+(\d+)(?:,(\d+))?", line)
|
||||
if match:
|
||||
start_line = int(match.group(1))
|
||||
line_count = 1
|
||||
if match.group(2):
|
||||
line_count = int(match.group(2))
|
||||
# The input is something like
|
||||
#
|
||||
# @@ -1, +0,0 @@
|
||||
#
|
||||
# which means no lines were added.
|
||||
if line_count == 0:
|
||||
continue
|
||||
# Also format lines range if line_count is 0 in case of deleting
|
||||
# surrounding statements.
|
||||
end_line = start_line
|
||||
if line_count != 0:
|
||||
end_line += line_count - 1
|
||||
lines_by_file.setdefault(filename, []).extend(
|
||||
["--lines", str(start_line) + ":" + str(end_line)]
|
||||
)
|
||||
|
||||
# Reformat files containing changes in place.
|
||||
has_diff = False
|
||||
for filename, lines in lines_by_file.items():
|
||||
if args.i and args.verbose:
|
||||
print("Formatting {}".format(filename))
|
||||
command = [args.binary, filename]
|
||||
if args.i:
|
||||
command.append("-i")
|
||||
if args.sort_includes:
|
||||
command.append("--sort-includes")
|
||||
command.extend(lines)
|
||||
if args.style:
|
||||
command.extend(["--style", args.style])
|
||||
if args.fallback_style:
|
||||
command.extend(["--fallback-style", args.fallback_style])
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=None,
|
||||
stdin=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
except OSError as e:
|
||||
# Give the user more context when clang-format isn't
|
||||
# found/isn't executable, etc.
|
||||
raise RuntimeError(
|
||||
'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)
|
||||
)
|
||||
|
||||
stdout, _stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
return p.returncode
|
||||
|
||||
if not args.i:
|
||||
with open(filename) as f:
|
||||
code = f.readlines()
|
||||
formatted_code = StringIO(stdout).readlines()
|
||||
diff = difflib.unified_diff(
|
||||
code,
|
||||
formatted_code,
|
||||
filename,
|
||||
filename,
|
||||
"(before formatting)",
|
||||
"(after formatting)",
|
||||
)
|
||||
diff_string = "".join(diff)
|
||||
if len(diff_string) > 0:
|
||||
has_diff = True
|
||||
sys.stdout.write(diff_string)
|
||||
|
||||
if has_diff:
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import initAsync from "./clang-format.js";
|
||||
|
||||
const wasm = new URL("./clang-format.wasm", import.meta.url);
|
||||
|
||||
export default function (init = fs.readFile(wasm)) {
|
||||
return initAsync(init);
|
||||
}
|
||||
|
||||
export * from "./clang-format.js";
|
||||
@@ -0,0 +1,8 @@
|
||||
import initAsync from "./clang-format.js";
|
||||
import wasm from "./clang-format.wasm?url";
|
||||
|
||||
export default function (input = wasm) {
|
||||
return initAsync(input);
|
||||
}
|
||||
|
||||
export * from "./clang-format.js";
|
||||
175
frontend/src/common/prettier/plugins/clang/clang-format.d.ts
vendored
Normal file
175
frontend/src/common/prettier/plugins/clang/clang-format.d.ts
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
export type InitInput =
|
||||
| RequestInfo
|
||||
| URL
|
||||
| Response
|
||||
| BufferSource
|
||||
| WebAssembly.Module;
|
||||
|
||||
export default function init(input?: InitInput): Promise<void>;
|
||||
|
||||
/**
|
||||
* The style to use for formatting.
|
||||
* Supported style values are:
|
||||
* - `LLVM` - A style complying with the LLVM coding standards.
|
||||
* - `Google` - A style complying with Google’s C++ style guide.
|
||||
* - `Chromium` - A style complying with Chromium’s style guide.
|
||||
* - `Mozilla` - A style complying with Mozilla’s style guide.
|
||||
* - `WebKit` - A style complying with WebKit’s style guide.
|
||||
* - `Microsoft` - A style complying with Microsoft’s style guide.
|
||||
* - `GNU` - A style complying with the GNU coding standards.
|
||||
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
|
||||
* - A string which represents `.clang-format` content.
|
||||
*
|
||||
*/
|
||||
export type Style =
|
||||
| "LLVM"
|
||||
| "Google"
|
||||
| "Chromium"
|
||||
| "Mozilla"
|
||||
| "WebKit"
|
||||
| "Microsoft"
|
||||
| "GNU"
|
||||
| (string & {});
|
||||
|
||||
/**
|
||||
* The filename to use for determining the language.
|
||||
*/
|
||||
export type Filename =
|
||||
| "main.c"
|
||||
| "main.cc"
|
||||
| "main.cxx"
|
||||
| "main.cpp"
|
||||
| "main.java"
|
||||
| "main.js"
|
||||
| "main.mjs"
|
||||
| "main.ts"
|
||||
| "main.json"
|
||||
| "main.m"
|
||||
| "main.mm"
|
||||
| "main.proto"
|
||||
| "main.cs"
|
||||
| (string & {});
|
||||
|
||||
/**
|
||||
* Formats the given content using the specified style.
|
||||
*
|
||||
* @param {string} content - The content to format.
|
||||
* @param {Filename} filename - The filename to use for determining the language.
|
||||
* @param {Style} style - The style to use for formatting.
|
||||
* Supported style values are:
|
||||
* - `LLVM` - A style complying with the LLVM coding standards.
|
||||
* - `Google` - A style complying with Google’s C++ style guide.
|
||||
* - `Chromium` - A style complying with Chromium’s style guide.
|
||||
* - `Mozilla` - A style complying with Mozilla’s style guide.
|
||||
* - `WebKit` - A style complying with WebKit’s style guide.
|
||||
* - `Microsoft` - A style complying with Microsoft’s style guide.
|
||||
* - `GNU` - A style complying with the GNU coding standards.
|
||||
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
|
||||
* - A string which represents `.clang-format` content.
|
||||
*
|
||||
* @returns {string} The formatted content.
|
||||
* @throws {Error}
|
||||
*
|
||||
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
|
||||
*/
|
||||
export declare function format(
|
||||
content: string,
|
||||
filename?: Filename,
|
||||
style?: Style,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Both the startLine and endLine are 1-based.
|
||||
*/
|
||||
export type LineRange = [startLine: number, endLine: number];
|
||||
|
||||
/**
|
||||
* Both the offset and length are measured in bytes.
|
||||
*/
|
||||
export type ByteRange = [offset: number, length: number];
|
||||
|
||||
/**
|
||||
* Formats the specified range of lines in the given content using the specified style.
|
||||
*
|
||||
* @param {string} content - The content to format.
|
||||
* @param {LineRange[]} range - Array<[startLine, endLine]> - The range of lines to format.
|
||||
* Both startLine and endLine are 1-based.
|
||||
* Multiple ranges can be formatted by specifying several lines arguments.
|
||||
* @param {Filename} filename - The filename to use for determining the language.
|
||||
* @param {Style} style - The style to use for formatting.
|
||||
* Supported style values are:
|
||||
* - `LLVM` - A style complying with the LLVM coding standards.
|
||||
* - `Google` - A style complying with Google’s C++ style guide.
|
||||
* - `Chromium` - A style complying with Chromium’s style guide.
|
||||
* - `Mozilla` - A style complying with Mozilla’s style guide.
|
||||
* - `WebKit` - A style complying with WebKit’s style guide.
|
||||
* - `Microsoft` - A style complying with Microsoft’s style guide.
|
||||
* - `GNU` - A style complying with the GNU coding standards.
|
||||
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
|
||||
* - A string which represents `.clang-format` content.
|
||||
*
|
||||
* @returns {string} The formatted content.
|
||||
* @throws {Error}
|
||||
*
|
||||
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
|
||||
*/
|
||||
export declare function format_line_range(
|
||||
content: string,
|
||||
range: ByteRange[] | [[offset: number]],
|
||||
filename?: Filename,
|
||||
style?: Style,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* @deprecated Use `format_line_range` instead.
|
||||
*/
|
||||
export declare function formatLineRange(
|
||||
content: string,
|
||||
range: ByteRange[] | [[offset: number]],
|
||||
filename?: Filename,
|
||||
style?: Style,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Formats the specified range of bytes in the given content using the specified style.
|
||||
*
|
||||
* @param {string} content - The content to format.
|
||||
* @param {ByteRange[]} range - Array<[offset, length]> - The range of bytes to format.
|
||||
* @param {Filename} filename - The filename to use for determining the language.
|
||||
* @param {Style} style - The style to use for formatting.
|
||||
* Supported style values are:
|
||||
* - `LLVM` - A style complying with the LLVM coding standards.
|
||||
* - `Google` - A style complying with Google’s C++ style guide.
|
||||
* - `Chromium` - A style complying with Chromium’s style guide.
|
||||
* - `Mozilla` - A style complying with Mozilla’s style guide.
|
||||
* - `WebKit` - A style complying with WebKit’s style guide.
|
||||
* - `Microsoft` - A style complying with Microsoft’s style guide.
|
||||
* - `GNU` - A style complying with the GNU coding standards.
|
||||
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
|
||||
* - A string which represents `.clang-format` content.
|
||||
*
|
||||
* @returns {string} The formatted content.
|
||||
* @throws {Error}
|
||||
*
|
||||
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
|
||||
*/
|
||||
export declare function format_byte_range(
|
||||
content: string,
|
||||
range: LineRange[],
|
||||
filename?: Filename,
|
||||
style?: Style,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* @deprecated Use `format_byte_range` instead.
|
||||
*/
|
||||
export declare function formatByteRange(
|
||||
content: string,
|
||||
range: LineRange[],
|
||||
filename?: Filename,
|
||||
style?: Style,
|
||||
): string;
|
||||
|
||||
export declare function version(): string;
|
||||
|
||||
export declare function set_fallback_style(style: Style): void;
|
||||
155
frontend/src/common/prettier/plugins/clang/clang-format.js
Normal file
155
frontend/src/common/prettier/plugins/clang/clang-format.js
Normal file
File diff suppressed because one or more lines are too long
BIN
frontend/src/common/prettier/plugins/clang/clang-format.wasm
Normal file
BIN
frontend/src/common/prettier/plugins/clang/clang-format.wasm
Normal file
Binary file not shown.
3
frontend/src/common/prettier/plugins/clang/cli-pre.js
Normal file
3
frontend/src/common/prettier/plugins/clang/cli-pre.js
Normal file
@@ -0,0 +1,3 @@
|
||||
Module.preRun = function customPreRun() {
|
||||
ENV.PWD = process.cwd();
|
||||
}
|
||||
748
frontend/src/common/prettier/plugins/clang/cli.cc
Normal file
748
frontend/src/common/prettier/plugins/clang/cli.cc
Normal file
@@ -0,0 +1,748 @@
|
||||
//===-- clang-format/ClangFormat.cpp - Clang format tool ------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// \file
|
||||
/// This file implements a clang-format tool that automatically formats
|
||||
/// (fragments of) C++ code.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "clang/../../lib/Format/MatchFilePath.h"
|
||||
#include "clang/Basic/Diagnostic.h"
|
||||
#include "clang/Basic/DiagnosticOptions.h"
|
||||
#include "clang/Basic/FileManager.h"
|
||||
#include "clang/Basic/SourceManager.h"
|
||||
#include "clang/Basic/Version.h"
|
||||
#include "clang/Format/Format.h"
|
||||
#include "clang/Rewrite/Core/Rewriter.h"
|
||||
#include "llvm/ADT/StringSwitch.h"
|
||||
#include "llvm/Support/CommandLine.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/InitLLVM.h"
|
||||
#include "llvm/Support/Process.h"
|
||||
#include <fstream>
|
||||
|
||||
#include "CustomFileSystem.h"
|
||||
|
||||
using namespace llvm;
|
||||
using clang::tooling::Replacements;
|
||||
|
||||
static cl::opt<bool> Help("h", cl::desc("Alias for -help"), cl::Hidden);
|
||||
|
||||
// Mark all our options with this category, everything else (except for -version
|
||||
// and -help) will be hidden.
|
||||
static cl::OptionCategory ClangFormatCategory("Clang-format options");
|
||||
|
||||
static cl::list<unsigned>
|
||||
Offsets("offset",
|
||||
cl::desc("Format a range starting at this byte offset.\n"
|
||||
"Multiple ranges can be formatted by specifying\n"
|
||||
"several -offset and -length pairs.\n"
|
||||
"Can only be used with one input file."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::list<unsigned>
|
||||
Lengths("length",
|
||||
cl::desc("Format a range of this length (in bytes).\n"
|
||||
"Multiple ranges can be formatted by specifying\n"
|
||||
"several -offset and -length pairs.\n"
|
||||
"When only a single -offset is specified without\n"
|
||||
"-length, clang-format will format up to the end\n"
|
||||
"of the file.\n"
|
||||
"Can only be used with one input file."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::list<std::string>
|
||||
LineRanges("lines",
|
||||
cl::desc("<start line>:<end line> - format a range of\n"
|
||||
"lines (both 1-based).\n"
|
||||
"Multiple ranges can be formatted by specifying\n"
|
||||
"several -lines arguments.\n"
|
||||
"Can't be used with -offset and -length.\n"
|
||||
"Can only be used with one input file."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::opt<std::string>
|
||||
Style("style", cl::desc(clang::format::StyleOptionHelpDescription),
|
||||
cl::init(clang::format::DefaultFormatStyle),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::opt<std::string>
|
||||
FallbackStyle("fallback-style",
|
||||
cl::desc("The name of the predefined style used as a\n"
|
||||
"fallback in case clang-format is invoked with\n"
|
||||
"-style=file, but can not find the .clang-format\n"
|
||||
"file to use. Defaults to 'LLVM'.\n"
|
||||
"Use -fallback-style=none to skip formatting."),
|
||||
cl::init(clang::format::DefaultFallbackStyle),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<std::string> AssumeFileName(
|
||||
"assume-filename",
|
||||
cl::desc("Set filename used to determine the language and to find\n"
|
||||
".clang-format file.\n"
|
||||
"Only used when reading from stdin.\n"
|
||||
"If this is not passed, the .clang-format file is searched\n"
|
||||
"relative to the current working directory when reading stdin.\n"
|
||||
"Unrecognized filenames are treated as C++.\n"
|
||||
"supported:\n"
|
||||
" CSharp: .cs\n"
|
||||
" Java: .java\n"
|
||||
" JavaScript: .js .mjs .cjs .ts\n"
|
||||
" Json: .json .ipynb\n"
|
||||
" Objective-C: .m .mm\n"
|
||||
" Proto: .proto .protodevel\n"
|
||||
" TableGen: .td\n"
|
||||
" TextProto: .txtpb .textpb .pb.txt .textproto .asciipb\n"
|
||||
" Verilog: .sv .svh .v .vh"),
|
||||
cl::init("<stdin>"), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool> Inplace("i",
|
||||
cl::desc("Inplace edit <file>s, if specified."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool> OutputXML("output-replacements-xml",
|
||||
cl::desc("Output replacements as XML."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::opt<bool>
|
||||
DumpConfig("dump-config",
|
||||
cl::desc("Dump configuration options to stdout and exit.\n"
|
||||
"Can be used with -style option."),
|
||||
cl::cat(ClangFormatCategory));
|
||||
static cl::opt<unsigned>
|
||||
Cursor("cursor",
|
||||
cl::desc("The position of the cursor when invoking\n"
|
||||
"clang-format from an editor integration"),
|
||||
cl::init(0), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool>
|
||||
SortIncludes("sort-includes",
|
||||
cl::desc("If set, overrides the include sorting behavior\n"
|
||||
"determined by the SortIncludes style flag"),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<std::string> QualifierAlignment(
|
||||
"qualifier-alignment",
|
||||
cl::desc("If set, overrides the qualifier alignment style\n"
|
||||
"determined by the QualifierAlignment style flag"),
|
||||
cl::init(""), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<std::string> Files(
|
||||
"files",
|
||||
cl::desc("A file containing a list of files to process, one per line."),
|
||||
cl::value_desc("filename"), cl::init(""), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool>
|
||||
Verbose("verbose", cl::desc("If set, shows the list of processed files"),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
// Use --dry-run to match other LLVM tools when you mean do it but don't
|
||||
// actually do it
|
||||
static cl::opt<bool>
|
||||
DryRun("dry-run",
|
||||
cl::desc("If set, do not actually make the formatting changes"),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
// Use -n as a common command as an alias for --dry-run. (git and make use -n)
|
||||
static cl::alias DryRunShort("n", cl::desc("Alias for --dry-run"),
|
||||
cl::cat(ClangFormatCategory), cl::aliasopt(DryRun),
|
||||
cl::NotHidden);
|
||||
|
||||
// Emulate being able to turn on/off the warning.
|
||||
static cl::opt<bool>
|
||||
WarnFormat("Wclang-format-violations",
|
||||
cl::desc("Warnings about individual formatting changes needed. "
|
||||
"Used only with --dry-run or -n"),
|
||||
cl::init(true), cl::cat(ClangFormatCategory), cl::Hidden);
|
||||
|
||||
static cl::opt<bool>
|
||||
NoWarnFormat("Wno-clang-format-violations",
|
||||
cl::desc("Do not warn about individual formatting changes "
|
||||
"needed. Used only with --dry-run or -n"),
|
||||
cl::init(false), cl::cat(ClangFormatCategory), cl::Hidden);
|
||||
|
||||
static cl::opt<unsigned> ErrorLimit(
|
||||
"ferror-limit",
|
||||
cl::desc("Set the maximum number of clang-format errors to emit\n"
|
||||
"before stopping (0 = no limit).\n"
|
||||
"Used only with --dry-run or -n"),
|
||||
cl::init(0), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool>
|
||||
WarningsAsErrors("Werror",
|
||||
cl::desc("If set, changes formatting warnings to errors"),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
namespace {
|
||||
enum class WNoError { Unknown };
|
||||
}
|
||||
|
||||
static cl::bits<WNoError> WNoErrorList(
|
||||
"Wno-error",
|
||||
cl::desc("If set, don't error out on the specified warning type."),
|
||||
cl::values(
|
||||
clEnumValN(WNoError::Unknown, "unknown",
|
||||
"If set, unknown format options are only warned about.\n"
|
||||
"This can be used to enable formatting, even if the\n"
|
||||
"configuration contains unknown (newer) options.\n"
|
||||
"Use with caution, as this might lead to dramatically\n"
|
||||
"differing format depending on an option being\n"
|
||||
"supported or not.")),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool>
|
||||
ShowColors("fcolor-diagnostics",
|
||||
cl::desc("If set, and on a color-capable terminal controls "
|
||||
"whether or not to print diagnostics in color"),
|
||||
cl::init(true), cl::cat(ClangFormatCategory), cl::Hidden);
|
||||
|
||||
static cl::opt<bool>
|
||||
NoShowColors("fno-color-diagnostics",
|
||||
cl::desc("If set, and on a color-capable terminal controls "
|
||||
"whether or not to print diagnostics in color"),
|
||||
cl::init(false), cl::cat(ClangFormatCategory), cl::Hidden);
|
||||
|
||||
static cl::list<std::string> FileNames(cl::Positional,
|
||||
cl::desc("[@<file>] [<file> ...]"),
|
||||
cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool> FailOnIncompleteFormat(
|
||||
"fail-on-incomplete-format",
|
||||
cl::desc("If set, fail with exit code 1 on incomplete format."),
|
||||
cl::init(false), cl::cat(ClangFormatCategory));
|
||||
|
||||
static cl::opt<bool> ListIgnored("list-ignored",
|
||||
cl::desc("List ignored files."),
|
||||
cl::cat(ClangFormatCategory), cl::Hidden);
|
||||
|
||||
namespace clang {
|
||||
namespace format {
|
||||
|
||||
static FileID createInMemoryFile(StringRef FileName, MemoryBufferRef Source,
|
||||
SourceManager &Sources, FileManager &Files,
|
||||
llvm::vfs::InMemoryFileSystem *MemFS) {
|
||||
MemFS->addFileNoOwn(FileName, 0, Source);
|
||||
auto File = Files.getOptionalFileRef(FileName);
|
||||
assert(File && "File not added to MemFS?");
|
||||
return Sources.createFileID(*File, SourceLocation(), SrcMgr::C_User);
|
||||
}
|
||||
|
||||
// Parses <start line>:<end line> input to a pair of line numbers.
|
||||
// Returns true on error.
|
||||
static bool parseLineRange(StringRef Input, unsigned &FromLine,
|
||||
unsigned &ToLine) {
|
||||
std::pair<StringRef, StringRef> LineRange = Input.split(':');
|
||||
return LineRange.first.getAsInteger(0, FromLine) ||
|
||||
LineRange.second.getAsInteger(0, ToLine);
|
||||
}
|
||||
|
||||
static bool fillRanges(MemoryBuffer *Code,
|
||||
std::vector<tooling::Range> &Ranges) {
|
||||
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
|
||||
new llvm::vfs::InMemoryFileSystem);
|
||||
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
|
||||
DiagnosticOptions DiagOpts;
|
||||
DiagnosticsEngine Diagnostics(
|
||||
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
|
||||
SourceManager Sources(Diagnostics, Files);
|
||||
const auto ID = createInMemoryFile("<irrelevant>", *Code, Sources, Files,
|
||||
InMemoryFileSystem.get());
|
||||
if (!LineRanges.empty()) {
|
||||
if (!Offsets.empty() || !Lengths.empty()) {
|
||||
errs() << "error: cannot use -lines with -offset/-length\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto &LineRange : LineRanges) {
|
||||
unsigned FromLine, ToLine;
|
||||
if (parseLineRange(LineRange, FromLine, ToLine)) {
|
||||
errs() << "error: invalid <start line>:<end line> pair\n";
|
||||
return true;
|
||||
}
|
||||
if (FromLine < 1) {
|
||||
errs() << "error: start line should be at least 1\n";
|
||||
return true;
|
||||
}
|
||||
if (FromLine > ToLine) {
|
||||
errs() << "error: start line should not exceed end line\n";
|
||||
return true;
|
||||
}
|
||||
const auto Start = Sources.translateLineCol(ID, FromLine, 1);
|
||||
const auto End = Sources.translateLineCol(ID, ToLine, UINT_MAX);
|
||||
if (Start.isInvalid() || End.isInvalid())
|
||||
return true;
|
||||
const auto Offset = Sources.getFileOffset(Start);
|
||||
const auto Length = Sources.getFileOffset(End) - Offset;
|
||||
Ranges.push_back(tooling::Range(Offset, Length));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Offsets.empty())
|
||||
Offsets.push_back(0);
|
||||
const bool EmptyLengths = Lengths.empty();
|
||||
unsigned Length = 0;
|
||||
if (Offsets.size() == 1 && EmptyLengths) {
|
||||
Length = Sources.getFileOffset(Sources.getLocForEndOfFile(ID)) - Offsets[0];
|
||||
} else if (Offsets.size() != Lengths.size()) {
|
||||
errs() << "error: number of -offset and -length arguments must match.\n";
|
||||
return true;
|
||||
}
|
||||
for (unsigned I = 0, E = Offsets.size(), CodeSize = Code->getBufferSize();
|
||||
I < E; ++I) {
|
||||
const auto Offset = Offsets[I];
|
||||
if (Offset >= CodeSize) {
|
||||
errs() << "error: offset " << Offset << " is outside the file\n";
|
||||
return true;
|
||||
}
|
||||
if (!EmptyLengths)
|
||||
Length = Lengths[I];
|
||||
if (Offset + Length > CodeSize) {
|
||||
errs() << "error: invalid length " << Length << ", offset + length ("
|
||||
<< Offset + Length << ") is outside the file.\n";
|
||||
return true;
|
||||
}
|
||||
Ranges.push_back(tooling::Range(Offset, Length));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void outputReplacementXML(StringRef Text) {
|
||||
// FIXME: When we sort includes, we need to make sure the stream is correct
|
||||
// utf-8.
|
||||
size_t From = 0;
|
||||
size_t Index;
|
||||
while ((Index = Text.find_first_of("\n\r<&", From)) != StringRef::npos) {
|
||||
outs() << Text.substr(From, Index - From);
|
||||
switch (Text[Index]) {
|
||||
case '\n':
|
||||
outs() << " ";
|
||||
break;
|
||||
case '\r':
|
||||
outs() << " ";
|
||||
break;
|
||||
case '<':
|
||||
outs() << "<";
|
||||
break;
|
||||
case '&':
|
||||
outs() << "&";
|
||||
break;
|
||||
default:
|
||||
llvm_unreachable("Unexpected character encountered!");
|
||||
}
|
||||
From = Index + 1;
|
||||
}
|
||||
outs() << Text.substr(From);
|
||||
}
|
||||
|
||||
static void outputReplacementsXML(const Replacements &Replaces) {
|
||||
for (const auto &R : Replaces) {
|
||||
outs() << "<replacement "
|
||||
<< "offset='" << R.getOffset() << "' "
|
||||
<< "length='" << R.getLength() << "'>";
|
||||
outputReplacementXML(R.getReplacementText());
|
||||
outs() << "</replacement>\n";
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
emitReplacementWarnings(const Replacements &Replaces, StringRef AssumedFileName,
|
||||
const std::unique_ptr<llvm::MemoryBuffer> &Code) {
|
||||
unsigned Errors = 0;
|
||||
if (WarnFormat && !NoWarnFormat) {
|
||||
SourceMgr Mgr;
|
||||
const char *StartBuf = Code->getBufferStart();
|
||||
|
||||
Mgr.AddNewSourceBuffer(
|
||||
MemoryBuffer::getMemBuffer(StartBuf, AssumedFileName), SMLoc());
|
||||
for (const auto &R : Replaces) {
|
||||
SMDiagnostic Diag = Mgr.GetMessage(
|
||||
SMLoc::getFromPointer(StartBuf + R.getOffset()),
|
||||
WarningsAsErrors ? SourceMgr::DiagKind::DK_Error
|
||||
: SourceMgr::DiagKind::DK_Warning,
|
||||
"code should be clang-formatted [-Wclang-format-violations]");
|
||||
|
||||
Diag.print(nullptr, llvm::errs(), ShowColors && !NoShowColors);
|
||||
if (ErrorLimit && ++Errors >= ErrorLimit)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return WarningsAsErrors;
|
||||
}
|
||||
|
||||
static void outputXML(const Replacements &Replaces,
|
||||
const Replacements &FormatChanges,
|
||||
const FormattingAttemptStatus &Status,
|
||||
const cl::opt<unsigned> &Cursor,
|
||||
unsigned CursorPosition) {
|
||||
outs() << "<?xml version='1.0'?>\n<replacements "
|
||||
"xml:space='preserve' incomplete_format='"
|
||||
<< (Status.FormatComplete ? "false" : "true") << "'";
|
||||
if (!Status.FormatComplete)
|
||||
outs() << " line='" << Status.Line << "'";
|
||||
outs() << ">\n";
|
||||
if (Cursor.getNumOccurrences() != 0) {
|
||||
outs() << "<cursor>" << FormatChanges.getShiftedCodePosition(CursorPosition)
|
||||
<< "</cursor>\n";
|
||||
}
|
||||
|
||||
outputReplacementsXML(Replaces);
|
||||
outs() << "</replacements>\n";
|
||||
}
|
||||
|
||||
class ClangFormatDiagConsumer : public DiagnosticConsumer {
|
||||
virtual void anchor() {}
|
||||
|
||||
void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
|
||||
const Diagnostic &Info) override {
|
||||
|
||||
SmallVector<char, 16> vec;
|
||||
Info.FormatDiagnostic(vec);
|
||||
errs() << "clang-format error:" << vec << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true on error.
|
||||
static bool format(StringRef FileName, bool ErrorOnIncompleteFormat = false) {
|
||||
const bool IsSTDIN = FileName == "-";
|
||||
if (!OutputXML && Inplace && IsSTDIN) {
|
||||
errs() << "error: cannot use -i when reading from stdin.\n";
|
||||
return true;
|
||||
}
|
||||
// On Windows, overwriting a file with an open file mapping doesn't work,
|
||||
// so read the whole file into memory when formatting in-place.
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
|
||||
!OutputXML && Inplace
|
||||
? MemoryBuffer::getFileAsStream(FileName)
|
||||
: MemoryBuffer::getFileOrSTDIN(FileName, /*IsText=*/true);
|
||||
if (std::error_code EC = CodeOrErr.getError()) {
|
||||
errs() << FileName << ": " << EC.message() << "\n";
|
||||
return true;
|
||||
}
|
||||
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
|
||||
if (Code->getBufferSize() == 0)
|
||||
return false; // Empty files are formatted correctly.
|
||||
|
||||
StringRef BufStr = Code->getBuffer();
|
||||
|
||||
const char *InvalidBOM = SrcMgr::ContentCache::getInvalidBOM(BufStr);
|
||||
|
||||
if (InvalidBOM) {
|
||||
errs() << "error: encoding with unsupported byte order mark \""
|
||||
<< InvalidBOM << "\" detected";
|
||||
if (!IsSTDIN)
|
||||
errs() << " in file '" << FileName << "'";
|
||||
errs() << ".\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<tooling::Range> Ranges;
|
||||
if (fillRanges(Code.get(), Ranges))
|
||||
return true;
|
||||
StringRef AssumedFileName = IsSTDIN ? AssumeFileName : FileName;
|
||||
if (AssumedFileName.empty()) {
|
||||
llvm::errs() << "error: empty filenames are not allowed\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
auto RealFS = vfs::getRealFileSystem();
|
||||
auto CustomFS = new vfs::CustomFileSystem(RealFS);
|
||||
IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
|
||||
Expected<FormatStyle> FormatStyle =
|
||||
getStyle(Style, AssumedFileName, FallbackStyle, Code->getBuffer(),
|
||||
CustomFSPtr.get(), WNoErrorList.isSet(WNoError::Unknown));
|
||||
if (!FormatStyle) {
|
||||
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
StringRef QualifierAlignmentOrder = QualifierAlignment;
|
||||
|
||||
FormatStyle->QualifierAlignment =
|
||||
StringSwitch<FormatStyle::QualifierAlignmentStyle>(
|
||||
QualifierAlignmentOrder.lower())
|
||||
.Case("right", FormatStyle::QAS_Right)
|
||||
.Case("left", FormatStyle::QAS_Left)
|
||||
.Default(FormatStyle->QualifierAlignment);
|
||||
|
||||
if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Left) {
|
||||
FormatStyle->QualifierOrder = {"const", "volatile", "type"};
|
||||
} else if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Right) {
|
||||
FormatStyle->QualifierOrder = {"type", "const", "volatile"};
|
||||
} else if (QualifierAlignmentOrder.contains("type")) {
|
||||
FormatStyle->QualifierAlignment = FormatStyle::QAS_Custom;
|
||||
SmallVector<StringRef> Qualifiers;
|
||||
QualifierAlignmentOrder.split(Qualifiers, " ", /*MaxSplit=*/-1,
|
||||
/*KeepEmpty=*/false);
|
||||
FormatStyle->QualifierOrder = {Qualifiers.begin(), Qualifiers.end()};
|
||||
}
|
||||
|
||||
if (SortIncludes.getNumOccurrences() != 0) {
|
||||
FormatStyle->SortIncludes = {};
|
||||
if (SortIncludes)
|
||||
FormatStyle->SortIncludes.Enabled = true;
|
||||
}
|
||||
unsigned CursorPosition = Cursor;
|
||||
Replacements Replaces = sortIncludes(*FormatStyle, Code->getBuffer(), Ranges,
|
||||
AssumedFileName, &CursorPosition);
|
||||
|
||||
const bool IsJson = FormatStyle->isJson();
|
||||
|
||||
// To format JSON insert a variable to trick the code into thinking its
|
||||
// JavaScript.
|
||||
if (IsJson && !FormatStyle->DisableFormat) {
|
||||
auto Err =
|
||||
Replaces.add(tooling::Replacement(AssumedFileName, 0, 0, "x = "));
|
||||
if (Err)
|
||||
llvm::errs() << "Bad Json variable insertion\n";
|
||||
}
|
||||
|
||||
auto ChangedCode = tooling::applyAllReplacements(Code->getBuffer(), Replaces);
|
||||
if (!ChangedCode) {
|
||||
llvm::errs() << toString(ChangedCode.takeError()) << "\n";
|
||||
return true;
|
||||
}
|
||||
// Get new affected ranges after sorting `#includes`.
|
||||
Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges);
|
||||
FormattingAttemptStatus Status;
|
||||
Replacements FormatChanges =
|
||||
reformat(*FormatStyle, *ChangedCode, Ranges, AssumedFileName, &Status);
|
||||
Replaces = Replaces.merge(FormatChanges);
|
||||
if (DryRun) {
|
||||
return Replaces.size() > (IsJson ? 1u : 0u) &&
|
||||
emitReplacementWarnings(Replaces, AssumedFileName, Code);
|
||||
}
|
||||
if (OutputXML) {
|
||||
outputXML(Replaces, FormatChanges, Status, Cursor, CursorPosition);
|
||||
} else {
|
||||
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
|
||||
new llvm::vfs::InMemoryFileSystem);
|
||||
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
|
||||
|
||||
DiagnosticOptions DiagOpts;
|
||||
ClangFormatDiagConsumer IgnoreDiagnostics;
|
||||
DiagnosticsEngine Diagnostics(
|
||||
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts,
|
||||
&IgnoreDiagnostics, false);
|
||||
SourceManager Sources(Diagnostics, Files);
|
||||
FileID ID = createInMemoryFile(AssumedFileName, *Code, Sources, Files,
|
||||
InMemoryFileSystem.get());
|
||||
Rewriter Rewrite(Sources, LangOptions());
|
||||
tooling::applyAllReplacements(Replaces, Rewrite);
|
||||
if (Inplace) {
|
||||
if (Rewrite.overwriteChangedFiles())
|
||||
return true;
|
||||
} else {
|
||||
if (Cursor.getNumOccurrences() != 0) {
|
||||
outs() << "{ \"Cursor\": "
|
||||
<< FormatChanges.getShiftedCodePosition(CursorPosition)
|
||||
<< ", \"IncompleteFormat\": "
|
||||
<< (Status.FormatComplete ? "false" : "true");
|
||||
if (!Status.FormatComplete)
|
||||
outs() << ", \"Line\": " << Status.Line;
|
||||
outs() << " }\n";
|
||||
}
|
||||
Rewrite.getEditBuffer(ID).write(outs());
|
||||
}
|
||||
}
|
||||
return ErrorOnIncompleteFormat && !Status.FormatComplete;
|
||||
}
|
||||
|
||||
} // namespace format
|
||||
} // namespace clang
|
||||
|
||||
static void PrintVersion(raw_ostream &OS) {
|
||||
OS << clang::getClangToolFullVersion("clang-format") << '\n';
|
||||
}
|
||||
|
||||
// Dump the configuration.
|
||||
static int dumpConfig() {
|
||||
std::unique_ptr<llvm::MemoryBuffer> Code;
|
||||
// We can't read the code to detect the language if there's no file name.
|
||||
if (!FileNames.empty()) {
|
||||
// Read in the code in case the filename alone isn't enough to detect the
|
||||
// language.
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
|
||||
MemoryBuffer::getFileOrSTDIN(FileNames[0], /*IsText=*/true);
|
||||
if (std::error_code EC = CodeOrErr.getError()) {
|
||||
llvm::errs() << EC.message() << "\n";
|
||||
return 1;
|
||||
}
|
||||
Code = std::move(CodeOrErr.get());
|
||||
}
|
||||
|
||||
auto RealFS = vfs::getRealFileSystem();
|
||||
auto CustomFS = new vfs::CustomFileSystem(RealFS);
|
||||
IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
|
||||
|
||||
Expected<clang::format::FormatStyle> FormatStyle = clang::format::getStyle(
|
||||
Style,
|
||||
FileNames.empty() || FileNames[0] == "-" ? AssumeFileName : FileNames[0],
|
||||
FallbackStyle, Code ? Code->getBuffer() : "", CustomFSPtr.get());
|
||||
if (!FormatStyle) {
|
||||
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::string Config = clang::format::configurationAsText(*FormatStyle);
|
||||
outs() << Config << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
using String = SmallString<128>;
|
||||
static String IgnoreDir; // Directory of .clang-format-ignore file.
|
||||
static String PrevDir; // Directory of previous `FilePath`.
|
||||
static SmallVector<String> Patterns; // Patterns in .clang-format-ignore file.
|
||||
|
||||
// Check whether `FilePath` is ignored according to the nearest
|
||||
// .clang-format-ignore file based on the rules below:
|
||||
// - A blank line is skipped.
|
||||
// - Leading and trailing spaces of a line are trimmed.
|
||||
// - A line starting with a hash (`#`) is a comment.
|
||||
// - A non-comment line is a single pattern.
|
||||
// - The slash (`/`) is used as the directory separator.
|
||||
// - A pattern is relative to the directory of the .clang-format-ignore file (or
|
||||
// the root directory if the pattern starts with a slash).
|
||||
// - A pattern is negated if it starts with a bang (`!`).
|
||||
static bool isIgnored(StringRef FilePath) {
|
||||
using namespace llvm::sys::fs;
|
||||
if (!is_regular_file(FilePath))
|
||||
return false;
|
||||
|
||||
String Path;
|
||||
String AbsPath{FilePath};
|
||||
|
||||
auto PathStyle = vfs::getPathStyle();
|
||||
|
||||
using namespace llvm::sys::path;
|
||||
vfs::make_absolute(AbsPath);
|
||||
remove_dots(AbsPath, /*remove_dot_dot=*/true, PathStyle);
|
||||
|
||||
if (StringRef Dir{parent_path(AbsPath, PathStyle)}; PrevDir != Dir) {
|
||||
PrevDir = Dir;
|
||||
|
||||
for (;;) {
|
||||
Path = Dir;
|
||||
append(Path, PathStyle, ".clang-format-ignore");
|
||||
if (is_regular_file(Path))
|
||||
break;
|
||||
Dir = parent_path(Dir, PathStyle);
|
||||
if (Dir.empty())
|
||||
return false;
|
||||
}
|
||||
|
||||
IgnoreDir = convert_to_slash(Dir, PathStyle);
|
||||
|
||||
std::ifstream IgnoreFile{Path.c_str()};
|
||||
if (!IgnoreFile.good())
|
||||
return false;
|
||||
|
||||
Patterns.clear();
|
||||
|
||||
for (std::string Line; std::getline(IgnoreFile, Line);) {
|
||||
if (const auto Pattern{StringRef{Line}.trim()};
|
||||
// Skip empty and comment lines.
|
||||
!Pattern.empty() && Pattern[0] != '#') {
|
||||
Patterns.push_back(Pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (IgnoreDir.empty())
|
||||
return false;
|
||||
|
||||
const auto Pathname{convert_to_slash(AbsPath, PathStyle)};
|
||||
for (const auto &Pat : Patterns) {
|
||||
const bool IsNegated = Pat[0] == '!';
|
||||
StringRef Pattern{Pat};
|
||||
if (IsNegated)
|
||||
Pattern = Pattern.drop_front();
|
||||
|
||||
if (Pattern.empty())
|
||||
continue;
|
||||
|
||||
Pattern = Pattern.ltrim();
|
||||
|
||||
// `Pattern` is relative to `IgnoreDir` unless it starts with a slash.
|
||||
// This doesn't support patterns containing drive names (e.g. `C:`).
|
||||
if (Pattern[0] != '/') {
|
||||
Path = IgnoreDir;
|
||||
append(Path, Style::posix, Pattern);
|
||||
remove_dots(Path, /*remove_dot_dot=*/true, Style::posix);
|
||||
Pattern = Path;
|
||||
}
|
||||
|
||||
if (clang::format::matchFilePath(Pattern, Pathname) == !IsNegated)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
InitLLVM X(argc, argv);
|
||||
|
||||
cl::HideUnrelatedOptions(ClangFormatCategory);
|
||||
|
||||
cl::SetVersionPrinter(PrintVersion);
|
||||
cl::ParseCommandLineOptions(
|
||||
argc, argv,
|
||||
"A tool to format C/C++/Java/JavaScript/JSON/Objective-C/Protobuf/C# "
|
||||
"code.\n\n"
|
||||
"If no arguments are specified, it formats the code from standard input\n"
|
||||
"and writes the result to the standard output.\n"
|
||||
"If <file>s are given, it reformats the files. If -i is specified\n"
|
||||
"together with <file>s, the files are edited in-place. Otherwise, the\n"
|
||||
"result is written to the standard output.\n");
|
||||
|
||||
if (Help) {
|
||||
cl::PrintHelpMessage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (DumpConfig)
|
||||
return dumpConfig();
|
||||
|
||||
if (!Files.empty()) {
|
||||
std::ifstream ExternalFileOfFiles{std::string(Files)};
|
||||
std::string Line;
|
||||
unsigned LineNo = 1;
|
||||
while (std::getline(ExternalFileOfFiles, Line)) {
|
||||
FileNames.push_back(Line);
|
||||
LineNo++;
|
||||
}
|
||||
errs() << "Clang-formatting " << LineNo << " files\n";
|
||||
}
|
||||
|
||||
if (FileNames.empty()) {
|
||||
if (isIgnored(AssumeFileName))
|
||||
return 0;
|
||||
return clang::format::format("-", FailOnIncompleteFormat);
|
||||
}
|
||||
|
||||
if (FileNames.size() > 1 &&
|
||||
(!Offsets.empty() || !Lengths.empty() || !LineRanges.empty())) {
|
||||
errs() << "error: -offset, -length and -lines can only be used for "
|
||||
"single file.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
unsigned FileNo = 1;
|
||||
bool Error = false;
|
||||
for (const auto &FileName : FileNames) {
|
||||
const bool Ignored = isIgnored(FileName);
|
||||
if (ListIgnored) {
|
||||
if (Ignored)
|
||||
outs() << FileName << '\n';
|
||||
continue;
|
||||
}
|
||||
if (Ignored)
|
||||
continue;
|
||||
if (Verbose) {
|
||||
errs() << "Formatting [" << FileNo++ << "/" << FileNames.size() << "] "
|
||||
<< FileName << "\n";
|
||||
}
|
||||
Error |= clang::format::format(FileName, FailOnIncompleteFormat);
|
||||
}
|
||||
return Error ? 1 : 0;
|
||||
}
|
||||
858
frontend/src/common/prettier/plugins/clang/git-clang-format
Normal file
858
frontend/src/common/prettier/plugins/clang/git-clang-format
Normal file
@@ -0,0 +1,858 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# ===- git-clang-format - ClangFormat Git Integration -------*- python -*--=== #
|
||||
#
|
||||
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
# See https://llvm.org/LICENSE.txt for license information.
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
#
|
||||
# ===----------------------------------------------------------------------=== #
|
||||
|
||||
r"""
|
||||
clang-format git integration
|
||||
============================
|
||||
|
||||
This file provides a clang-format integration for git. Put it somewhere in your
|
||||
path and ensure that it is executable. Then, "git clang-format" will invoke
|
||||
clang-format on the changes in current files or a specific commit.
|
||||
|
||||
For further details, run:
|
||||
git clang-format -h
|
||||
|
||||
Requires Python version >=3.8
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import collections
|
||||
import contextlib
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
usage = "git clang-format [OPTIONS] [<commit>] [<commit>|--staged] [--] [<file>...]"
|
||||
|
||||
desc = """
|
||||
If zero or one commits are given, run clang-format on all lines that differ
|
||||
between the working directory and <commit>, which defaults to HEAD. Changes are
|
||||
only applied to the working directory, or in the stage/index.
|
||||
|
||||
Examples:
|
||||
To format staged changes, i.e everything that's been `git add`ed:
|
||||
git clang-format
|
||||
|
||||
To also format everything touched in the most recent commit:
|
||||
git clang-format HEAD~1
|
||||
|
||||
If you're on a branch off main, to format everything touched on your branch:
|
||||
git clang-format main
|
||||
|
||||
If two commits are given (requires --diff), run clang-format on all lines in the
|
||||
second <commit> that differ from the first <commit>.
|
||||
|
||||
The following git-config settings set the default of the corresponding option:
|
||||
clangFormat.binary
|
||||
clangFormat.commit
|
||||
clangFormat.extensions
|
||||
clangFormat.style
|
||||
"""
|
||||
|
||||
# Name of the temporary index file in which save the output of clang-format.
|
||||
# This file is created within the .git directory.
|
||||
temp_index_basename = "clang-format-index"
|
||||
|
||||
|
||||
Range = collections.namedtuple("Range", "start, count")
|
||||
|
||||
|
||||
def main():
|
||||
config = load_git_config()
|
||||
|
||||
# In order to keep '--' yet allow options after positionals, we need to
|
||||
# check for '--' ourselves. (Setting nargs='*' throws away the '--', while
|
||||
# nargs=argparse.REMAINDER disallows options after positionals.)
|
||||
argv = sys.argv[1:]
|
||||
try:
|
||||
idx = argv.index("--")
|
||||
except ValueError:
|
||||
dash_dash = []
|
||||
else:
|
||||
dash_dash = argv[idx:]
|
||||
argv = argv[:idx]
|
||||
|
||||
default_extensions = ",".join(
|
||||
[
|
||||
# From clang/lib/Frontend/FrontendOptions.cpp, all lower case
|
||||
"c",
|
||||
"h", # C
|
||||
"m", # ObjC
|
||||
"mm", # ObjC++
|
||||
"cc",
|
||||
"cp",
|
||||
"cpp",
|
||||
"c++",
|
||||
"cxx",
|
||||
"hh",
|
||||
"hpp",
|
||||
"hxx",
|
||||
"inc", # C++
|
||||
"ccm",
|
||||
"cppm",
|
||||
"cxxm",
|
||||
"c++m", # C++ Modules
|
||||
"cu",
|
||||
"cuh", # CUDA
|
||||
"cl", # OpenCL
|
||||
# Other languages that clang-format supports
|
||||
"proto",
|
||||
"protodevel", # Protocol Buffers
|
||||
"java", # Java
|
||||
"js",
|
||||
"mjs",
|
||||
"cjs", # JavaScript
|
||||
"ts", # TypeScript
|
||||
"cs", # C Sharp
|
||||
"json",
|
||||
"ipynb", # Json
|
||||
"sv",
|
||||
"svh",
|
||||
"v",
|
||||
"vh", # Verilog
|
||||
"td", # TableGen
|
||||
"txtpb",
|
||||
"textpb",
|
||||
"pb.txt",
|
||||
"textproto",
|
||||
"asciipb", # TextProto
|
||||
]
|
||||
)
|
||||
|
||||
p = argparse.ArgumentParser(
|
||||
usage=usage,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=desc,
|
||||
)
|
||||
p.add_argument(
|
||||
"--binary",
|
||||
default=config.get("clangformat.binary", "clang-format"),
|
||||
help="path to clang-format",
|
||||
),
|
||||
p.add_argument(
|
||||
"--commit",
|
||||
default=config.get("clangformat.commit", "HEAD"),
|
||||
help="default commit to use if none is specified",
|
||||
),
|
||||
p.add_argument(
|
||||
"--diff",
|
||||
action="store_true",
|
||||
help="print a diff instead of applying the changes",
|
||||
)
|
||||
p.add_argument(
|
||||
"--diffstat",
|
||||
action="store_true",
|
||||
help="print a diffstat instead of applying the changes",
|
||||
)
|
||||
p.add_argument(
|
||||
"--extensions",
|
||||
default=config.get("clangformat.extensions", default_extensions),
|
||||
help=(
|
||||
"comma-separated list of file extensions to format, "
|
||||
"excluding the period and case-insensitive"
|
||||
),
|
||||
),
|
||||
p.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="allow changes to unstaged files",
|
||||
)
|
||||
p.add_argument(
|
||||
"-p", "--patch", action="store_true", help="select hunks interactively"
|
||||
)
|
||||
p.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
action="count",
|
||||
default=0,
|
||||
help="print less information",
|
||||
)
|
||||
p.add_argument(
|
||||
"--staged",
|
||||
"--cached",
|
||||
action="store_true",
|
||||
help="format lines in the stage instead of the working dir",
|
||||
)
|
||||
p.add_argument(
|
||||
"--style",
|
||||
default=config.get("clangformat.style", None),
|
||||
help="passed to clang-format",
|
||||
),
|
||||
p.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="print extra information",
|
||||
)
|
||||
p.add_argument(
|
||||
"--diff_from_common_commit",
|
||||
action="store_true",
|
||||
help=(
|
||||
"diff from the last common commit for commits in "
|
||||
"separate branches rather than the exact point of the "
|
||||
"commits"
|
||||
),
|
||||
)
|
||||
# We gather all the remaining positional arguments into 'args' since we need
|
||||
# to use some heuristics to determine whether or not <commit> was present.
|
||||
# However, to print pretty messages, we make use of metavar and help.
|
||||
p.add_argument(
|
||||
"args",
|
||||
nargs="*",
|
||||
metavar="<commit>",
|
||||
help="revision from which to compute the diff",
|
||||
)
|
||||
p.add_argument(
|
||||
"ignored",
|
||||
nargs="*",
|
||||
metavar="<file>...",
|
||||
help="if specified, only consider differences in these files",
|
||||
)
|
||||
opts = p.parse_args(argv)
|
||||
|
||||
opts.verbose -= opts.quiet
|
||||
del opts.quiet
|
||||
|
||||
commits, files = interpret_args(opts.args, dash_dash, opts.commit)
|
||||
if len(commits) > 2:
|
||||
die("at most two commits allowed; %d given" % len(commits))
|
||||
if len(commits) == 2:
|
||||
if opts.staged:
|
||||
die("--staged is not allowed when two commits are given")
|
||||
if not opts.diff:
|
||||
die("--diff is required when two commits are given")
|
||||
elif opts.diff_from_common_commit:
|
||||
die("--diff_from_common_commit is only allowed when two commits are given")
|
||||
|
||||
if os.path.dirname(opts.binary):
|
||||
opts.binary = os.path.abspath(opts.binary)
|
||||
|
||||
changed_lines = compute_diff_and_extract_lines(
|
||||
commits, files, opts.staged, opts.diff_from_common_commit
|
||||
)
|
||||
if opts.verbose >= 1:
|
||||
ignored_files = set(changed_lines)
|
||||
filter_by_extension(changed_lines, opts.extensions.lower().split(","))
|
||||
# The computed diff outputs absolute paths, so we must cd before accessing
|
||||
# those files.
|
||||
cd_to_toplevel()
|
||||
filter_symlinks(changed_lines)
|
||||
filter_ignored_files(changed_lines, binary=opts.binary)
|
||||
if opts.verbose >= 1:
|
||||
ignored_files.difference_update(changed_lines)
|
||||
if ignored_files:
|
||||
print(
|
||||
"Ignoring the following files (wrong extension, symlink, or "
|
||||
"ignored by clang-format):"
|
||||
)
|
||||
for filename in ignored_files:
|
||||
print(" %s" % filename)
|
||||
if changed_lines:
|
||||
print("Running clang-format on the following files:")
|
||||
for filename in changed_lines:
|
||||
print(" %s" % filename)
|
||||
|
||||
if not changed_lines:
|
||||
if opts.verbose >= 0:
|
||||
print("no modified files to format")
|
||||
return 0
|
||||
|
||||
if len(commits) > 1:
|
||||
old_tree = commits[1]
|
||||
revision = old_tree
|
||||
elif opts.staged:
|
||||
old_tree = create_tree_from_index(changed_lines)
|
||||
revision = ""
|
||||
else:
|
||||
old_tree = create_tree_from_workdir(changed_lines)
|
||||
revision = None
|
||||
new_tree = run_clang_format_and_save_to_tree(
|
||||
changed_lines, revision, binary=opts.binary, style=opts.style
|
||||
)
|
||||
if opts.verbose >= 1:
|
||||
print("old tree: %s" % old_tree)
|
||||
print("new tree: %s" % new_tree)
|
||||
|
||||
if old_tree == new_tree:
|
||||
if opts.verbose >= 0:
|
||||
print("clang-format did not modify any files")
|
||||
return 0
|
||||
|
||||
if opts.diff:
|
||||
return print_diff(old_tree, new_tree)
|
||||
if opts.diffstat:
|
||||
return print_diffstat(old_tree, new_tree)
|
||||
|
||||
changed_files = apply_changes(
|
||||
old_tree, new_tree, force=opts.force, patch_mode=opts.patch
|
||||
)
|
||||
if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
|
||||
print("changed files:")
|
||||
for filename in changed_files:
|
||||
print(" %s" % filename)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def load_git_config(non_string_options=None):
|
||||
"""Return the git configuration as a dictionary.
|
||||
|
||||
All options are assumed to be strings unless in `non_string_options`, in
|
||||
which is a dictionary mapping option name (in lower case) to either "--bool"
|
||||
or "--int"."""
|
||||
if non_string_options is None:
|
||||
non_string_options = {}
|
||||
out = {}
|
||||
for entry in run("git", "config", "--list", "--null").split("\0"):
|
||||
if entry:
|
||||
if "\n" in entry:
|
||||
name, value = entry.split("\n", 1)
|
||||
else:
|
||||
# A setting with no '=' ('\n' with --null) is implicitly 'true'
|
||||
name = entry
|
||||
value = "true"
|
||||
if name in non_string_options:
|
||||
value = run("git", "config", non_string_options[name], name)
|
||||
out[name] = value
|
||||
return out
|
||||
|
||||
|
||||
def interpret_args(args, dash_dash, default_commit):
|
||||
"""Interpret `args` as "[commits] [--] [files]" and return (commits, files).
|
||||
|
||||
It is assumed that "--" and everything that follows has been removed from
|
||||
args and placed in `dash_dash`.
|
||||
|
||||
If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its
|
||||
left (if present) are taken as commits. Otherwise, the arguments are
|
||||
checked from left to right if they are commits or files. If commits are not
|
||||
given, a list with `default_commit` is used."""
|
||||
if dash_dash:
|
||||
if len(args) == 0:
|
||||
commits = [default_commit]
|
||||
else:
|
||||
commits = args
|
||||
for commit in commits:
|
||||
object_type = get_object_type(commit)
|
||||
if object_type not in ("commit", "tag"):
|
||||
if object_type is None:
|
||||
die("'%s' is not a commit" % commit)
|
||||
else:
|
||||
die(
|
||||
"'%s' is a %s, but a commit was expected"
|
||||
% (commit, object_type)
|
||||
)
|
||||
files = dash_dash[1:]
|
||||
elif args:
|
||||
commits = []
|
||||
while args:
|
||||
if not disambiguate_revision(args[0]):
|
||||
break
|
||||
commits.append(args.pop(0))
|
||||
if not commits:
|
||||
commits = [default_commit]
|
||||
files = args
|
||||
else:
|
||||
commits = [default_commit]
|
||||
files = []
|
||||
return commits, files
|
||||
|
||||
|
||||
def disambiguate_revision(value):
|
||||
"""Returns True if `value` is a revision, False if it is a file, or dies."""
|
||||
# If `value` is ambiguous (neither a commit nor a file), the following
|
||||
# command will die with an appropriate error message.
|
||||
run("git", "rev-parse", value, verbose=False)
|
||||
object_type = get_object_type(value)
|
||||
if object_type is None:
|
||||
return False
|
||||
if object_type in ("commit", "tag"):
|
||||
return True
|
||||
die("`%s` is a %s, but a commit or filename was expected" % (value, object_type))
|
||||
|
||||
|
||||
def get_object_type(value):
|
||||
"""Returns a string description of an object's type, or None if it is not
|
||||
a valid git object."""
|
||||
cmd = ["git", "cat-file", "-t", value]
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
return None
|
||||
return convert_string(stdout.strip())
|
||||
|
||||
|
||||
def compute_diff_and_extract_lines(commits, files, staged, diff_common_commit):
|
||||
"""Calls compute_diff() followed by extract_lines()."""
|
||||
diff_process = compute_diff(commits, files, staged, diff_common_commit)
|
||||
changed_lines = extract_lines(diff_process.stdout)
|
||||
diff_process.stdout.close()
|
||||
diff_process.wait()
|
||||
if diff_process.returncode != 0:
|
||||
# Assume error was already printed to stderr.
|
||||
sys.exit(2)
|
||||
return changed_lines
|
||||
|
||||
|
||||
def compute_diff(commits, files, staged, diff_common_commit):
|
||||
"""Return a subprocess object producing the diff from `commits`.
|
||||
|
||||
The return value's `stdin` file object will produce a patch with the
|
||||
differences between the working directory (or stage if --staged is used) and
|
||||
the first commit if a single one was specified, or the difference between
|
||||
both specified commits, filtered on `files` (if non-empty).
|
||||
Zero context lines are used in the patch."""
|
||||
git_tool = "diff-index"
|
||||
extra_args = []
|
||||
if len(commits) == 2:
|
||||
git_tool = "diff-tree"
|
||||
if diff_common_commit:
|
||||
commits = [f"{commits[0]}...{commits[1]}"]
|
||||
elif staged:
|
||||
extra_args += ["--cached"]
|
||||
|
||||
cmd = ["git", git_tool, "-p", "-U0"] + extra_args + commits + ["--"]
|
||||
cmd.extend(files)
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
p.stdin.close()
|
||||
return p
|
||||
|
||||
|
||||
def extract_lines(patch_file):
|
||||
"""Extract the changed lines in `patch_file`.
|
||||
|
||||
The return value is a dictionary mapping filename to a list of (start_line,
|
||||
line_count) pairs.
|
||||
|
||||
The input must have been produced with ``-U0``, meaning unidiff format with
|
||||
zero lines of context. The return value is a dict mapping filename to a
|
||||
list of line `Range`s."""
|
||||
matches = {}
|
||||
for line in patch_file:
|
||||
line = convert_string(line)
|
||||
match = re.search(r"^\+\+\+\ [^/]+/(.*)", line)
|
||||
if match:
|
||||
filename = match.group(1).rstrip("\r\n\t")
|
||||
match = re.search(r"^@@ -[0-9,]+ \+(\d+)(,(\d+))?", line)
|
||||
if match:
|
||||
start_line = int(match.group(1))
|
||||
line_count = 1
|
||||
if match.group(3):
|
||||
line_count = int(match.group(3))
|
||||
if line_count == 0:
|
||||
line_count = 1
|
||||
if start_line == 0:
|
||||
continue
|
||||
matches.setdefault(filename, []).append(Range(start_line, line_count))
|
||||
return matches
|
||||
|
||||
|
||||
def filter_by_extension(dictionary, allowed_extensions):
|
||||
"""Delete every key in `dictionary` that doesn't have an allowed extension.
|
||||
|
||||
`allowed_extensions` must be a collection of lowercase file extensions,
|
||||
excluding the period."""
|
||||
allowed_extensions = frozenset(allowed_extensions)
|
||||
for filename in list(dictionary.keys()):
|
||||
base_ext = filename.rsplit(".", 1)
|
||||
if len(base_ext) == 1 and "" in allowed_extensions:
|
||||
continue
|
||||
if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
|
||||
del dictionary[filename]
|
||||
|
||||
|
||||
def filter_symlinks(dictionary):
|
||||
"""Delete every key in `dictionary` that is a symlink."""
|
||||
for filename in list(dictionary.keys()):
|
||||
if os.path.islink(filename):
|
||||
del dictionary[filename]
|
||||
|
||||
|
||||
def filter_ignored_files(dictionary, binary):
|
||||
"""Delete every key in `dictionary` that is ignored by clang-format."""
|
||||
ignored_files = run(binary, "-list-ignored", *dictionary.keys())
|
||||
if not ignored_files:
|
||||
return
|
||||
ignored_files = ignored_files.split("\n")
|
||||
for filename in ignored_files:
|
||||
del dictionary[filename]
|
||||
|
||||
|
||||
def cd_to_toplevel():
|
||||
"""Change to the top level of the git repository."""
|
||||
toplevel = run("git", "rev-parse", "--show-toplevel")
|
||||
os.chdir(toplevel)
|
||||
|
||||
|
||||
def create_tree_from_workdir(filenames):
|
||||
"""Create a new git tree with the given files from the working directory.
|
||||
|
||||
Returns the object ID (SHA-1) of the created tree."""
|
||||
return create_tree(filenames, "--stdin")
|
||||
|
||||
|
||||
def create_tree_from_index(filenames):
|
||||
# Copy the environment, because the files have to be read from the original
|
||||
# index.
|
||||
env = os.environ.copy()
|
||||
|
||||
def index_contents_generator():
|
||||
for filename in filenames:
|
||||
git_ls_files_cmd = [
|
||||
"git",
|
||||
"ls-files",
|
||||
"--stage",
|
||||
"-z",
|
||||
"--",
|
||||
filename,
|
||||
]
|
||||
git_ls_files = subprocess.Popen(
|
||||
git_ls_files_cmd,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
stdout = git_ls_files.communicate()[0]
|
||||
yield convert_string(stdout.split(b"\0")[0])
|
||||
|
||||
return create_tree(index_contents_generator(), "--index-info")
|
||||
|
||||
|
||||
def run_clang_format_and_save_to_tree(
|
||||
changed_lines, revision=None, binary="clang-format", style=None
|
||||
):
|
||||
"""Run clang-format on each file and save the result to a git tree.
|
||||
|
||||
Returns the object ID (SHA-1) of the created tree."""
|
||||
# Copy the environment when formatting the files in the index, because the
|
||||
# files have to be read from the original index.
|
||||
env = os.environ.copy() if revision == "" else None
|
||||
|
||||
def iteritems(container):
|
||||
try:
|
||||
return container.iteritems() # Python 2
|
||||
except AttributeError:
|
||||
return container.items() # Python 3
|
||||
|
||||
def index_info_generator():
|
||||
for filename, line_ranges in iteritems(changed_lines):
|
||||
if revision is not None:
|
||||
if len(revision) > 0:
|
||||
git_metadata_cmd = [
|
||||
"git",
|
||||
"ls-tree",
|
||||
"%s:%s" % (revision, os.path.dirname(filename)),
|
||||
os.path.basename(filename),
|
||||
]
|
||||
else:
|
||||
git_metadata_cmd = [
|
||||
"git",
|
||||
"ls-files",
|
||||
"--stage",
|
||||
"--",
|
||||
filename,
|
||||
]
|
||||
git_metadata = subprocess.Popen(
|
||||
git_metadata_cmd,
|
||||
env=env,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
stdout = git_metadata.communicate()[0]
|
||||
mode = oct(int(stdout.split()[0], 8))
|
||||
else:
|
||||
mode = oct(os.stat(filename).st_mode)
|
||||
# Adjust python3 octal format so that it matches what git expects
|
||||
if mode.startswith("0o"):
|
||||
mode = "0" + mode[2:]
|
||||
blob_id = clang_format_to_blob(
|
||||
filename,
|
||||
line_ranges,
|
||||
revision=revision,
|
||||
binary=binary,
|
||||
style=style,
|
||||
env=env,
|
||||
)
|
||||
yield "%s %s\t%s" % (mode, blob_id, filename)
|
||||
|
||||
return create_tree(index_info_generator(), "--index-info")
|
||||
|
||||
|
||||
def create_tree(input_lines, mode):
|
||||
"""Create a tree object from the given input.
|
||||
|
||||
If mode is '--stdin', it must be a list of filenames. If mode is
|
||||
'--index-info' is must be a list of values suitable for "git update-index
|
||||
--index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other
|
||||
mode is invalid."""
|
||||
assert mode in ("--stdin", "--index-info")
|
||||
cmd = ["git", "update-index", "--add", "-z", mode]
|
||||
with temporary_index_file():
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
||||
for line in input_lines:
|
||||
p.stdin.write(to_bytes("%s\0" % line))
|
||||
p.stdin.close()
|
||||
if p.wait() != 0:
|
||||
die("`%s` failed" % " ".join(cmd))
|
||||
tree_id = run("git", "write-tree")
|
||||
return tree_id
|
||||
|
||||
|
||||
def clang_format_to_blob(
|
||||
filename,
|
||||
line_ranges,
|
||||
revision=None,
|
||||
binary="clang-format",
|
||||
style=None,
|
||||
env=None,
|
||||
):
|
||||
"""Run clang-format on the given file and save the result to a git blob.
|
||||
|
||||
Runs on the file in `revision` if not None, or on the file in the working
|
||||
directory if `revision` is None. Revision can be set to an empty string to
|
||||
run clang-format on the file in the index.
|
||||
|
||||
Returns the object ID (SHA-1) of the created blob."""
|
||||
clang_format_cmd = [binary]
|
||||
if style:
|
||||
clang_format_cmd.extend(["--style=" + style])
|
||||
clang_format_cmd.extend(
|
||||
[
|
||||
"--lines=%s:%s" % (start_line, start_line + line_count - 1)
|
||||
for start_line, line_count in line_ranges
|
||||
]
|
||||
)
|
||||
if revision is not None:
|
||||
clang_format_cmd.extend(["--assume-filename=" + filename])
|
||||
git_show_cmd = [
|
||||
"git",
|
||||
"cat-file",
|
||||
"blob",
|
||||
"%s:%s" % (revision, filename),
|
||||
]
|
||||
git_show = subprocess.Popen(
|
||||
git_show_cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||
)
|
||||
git_show.stdin.close()
|
||||
clang_format_stdin = git_show.stdout
|
||||
else:
|
||||
clang_format_cmd.extend([filename])
|
||||
git_show = None
|
||||
clang_format_stdin = subprocess.PIPE
|
||||
try:
|
||||
clang_format = subprocess.Popen(
|
||||
clang_format_cmd, stdin=clang_format_stdin, stdout=subprocess.PIPE
|
||||
)
|
||||
if clang_format_stdin == subprocess.PIPE:
|
||||
clang_format_stdin = clang_format.stdin
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
die('cannot find executable "%s"' % binary)
|
||||
else:
|
||||
raise
|
||||
clang_format_stdin.close()
|
||||
hash_object_cmd = [
|
||||
"git",
|
||||
"hash-object",
|
||||
"-w",
|
||||
"--path=" + filename,
|
||||
"--stdin",
|
||||
]
|
||||
hash_object = subprocess.Popen(
|
||||
hash_object_cmd, stdin=clang_format.stdout, stdout=subprocess.PIPE
|
||||
)
|
||||
clang_format.stdout.close()
|
||||
stdout = hash_object.communicate()[0]
|
||||
if hash_object.returncode != 0:
|
||||
die("`%s` failed" % " ".join(hash_object_cmd))
|
||||
if clang_format.wait() != 0:
|
||||
die("`%s` failed" % " ".join(clang_format_cmd))
|
||||
if git_show and git_show.wait() != 0:
|
||||
die("`%s` failed" % " ".join(git_show_cmd))
|
||||
return convert_string(stdout).rstrip("\r\n")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def temporary_index_file(tree=None):
|
||||
"""Context manager for setting GIT_INDEX_FILE to a temporary file and
|
||||
deleting the file afterward."""
|
||||
index_path = create_temporary_index(tree)
|
||||
old_index_path = os.environ.get("GIT_INDEX_FILE")
|
||||
os.environ["GIT_INDEX_FILE"] = index_path
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if old_index_path is None:
|
||||
del os.environ["GIT_INDEX_FILE"]
|
||||
else:
|
||||
os.environ["GIT_INDEX_FILE"] = old_index_path
|
||||
os.remove(index_path)
|
||||
|
||||
|
||||
def create_temporary_index(tree=None):
|
||||
"""Create a temporary index file and return the created file's path.
|
||||
|
||||
If `tree` is not None, use that as the tree to read in. Otherwise, an
|
||||
empty index is created."""
|
||||
gitdir = run("git", "rev-parse", "--git-dir")
|
||||
path = os.path.join(gitdir, temp_index_basename)
|
||||
if tree is None:
|
||||
tree = "--empty"
|
||||
run("git", "read-tree", "--index-output=" + path, tree)
|
||||
return path
|
||||
|
||||
|
||||
def print_diff(old_tree, new_tree):
|
||||
"""Print the diff between the two trees to stdout."""
|
||||
# We use the porcelain 'diff' and not plumbing 'diff-tree' because the
|
||||
# output is expected to be viewed by the user, and only the former does nice
|
||||
# things like color and pagination.
|
||||
#
|
||||
# We also only print modified files since `new_tree` only contains the files
|
||||
# that were modified, so unmodified files would show as deleted without the
|
||||
# filter.
|
||||
return subprocess.run(
|
||||
["git", "diff", "--diff-filter=M", "--exit-code", old_tree, new_tree]
|
||||
).returncode
|
||||
|
||||
|
||||
def print_diffstat(old_tree, new_tree):
|
||||
"""Print the diffstat between the two trees to stdout."""
|
||||
# We use the porcelain 'diff' and not plumbing 'diff-tree' because the
|
||||
# output is expected to be viewed by the user, and only the former does nice
|
||||
# things like color and pagination.
|
||||
#
|
||||
# We also only print modified files since `new_tree` only contains the files
|
||||
# that were modified, so unmodified files would show as deleted without the
|
||||
# filter.
|
||||
return subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--diff-filter=M",
|
||||
"--exit-code",
|
||||
"--stat",
|
||||
old_tree,
|
||||
new_tree,
|
||||
]
|
||||
).returncode
|
||||
|
||||
|
||||
def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
|
||||
"""Apply the changes in `new_tree` to the working directory.
|
||||
|
||||
Bails if there are local changes in those files and not `force`. If
|
||||
`patch_mode`, runs `git checkout --patch` to select hunks interactively."""
|
||||
changed_files = (
|
||||
run(
|
||||
"git",
|
||||
"diff-tree",
|
||||
"--diff-filter=M",
|
||||
"-r",
|
||||
"-z",
|
||||
"--name-only",
|
||||
old_tree,
|
||||
new_tree,
|
||||
)
|
||||
.rstrip("\0")
|
||||
.split("\0")
|
||||
)
|
||||
if not force:
|
||||
unstaged_files = run("git", "diff-files", "--name-status", *changed_files)
|
||||
if unstaged_files:
|
||||
print(
|
||||
"The following files would be modified but have unstaged changes:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(unstaged_files, file=sys.stderr)
|
||||
print("Please commit, stage, or stash them first.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if patch_mode:
|
||||
# In patch mode, we could just as well create an index from the new tree
|
||||
# and checkout from that, but then the user will be presented with a
|
||||
# message saying "Discard ... from worktree". Instead, we use the old
|
||||
# tree as the index and checkout from new_tree, which gives the slightly
|
||||
# better message, "Apply ... to index and worktree". This is not quite
|
||||
# right, since it won't be applied to the user's index, but oh well.
|
||||
with temporary_index_file(old_tree):
|
||||
subprocess.run(["git", "checkout", "--patch", new_tree], check=True)
|
||||
index_tree = old_tree
|
||||
else:
|
||||
with temporary_index_file(new_tree):
|
||||
run("git", "checkout-index", "-f", "--", *changed_files)
|
||||
return changed_files
|
||||
|
||||
|
||||
def run(*args, **kwargs):
|
||||
stdin = kwargs.pop("stdin", "")
|
||||
verbose = kwargs.pop("verbose", True)
|
||||
strip = kwargs.pop("strip", True)
|
||||
for name in kwargs:
|
||||
raise TypeError("run() got an unexpected keyword argument '%s'" % name)
|
||||
p = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = p.communicate(input=stdin)
|
||||
|
||||
stdout = convert_string(stdout)
|
||||
stderr = convert_string(stderr)
|
||||
|
||||
if p.returncode == 0:
|
||||
if stderr:
|
||||
if verbose:
|
||||
print("`%s` printed to stderr:" % " ".join(args), file=sys.stderr)
|
||||
print(stderr.rstrip(), file=sys.stderr)
|
||||
if strip:
|
||||
stdout = stdout.rstrip("\r\n")
|
||||
return stdout
|
||||
if verbose:
|
||||
print("`%s` returned %s" % (" ".join(args), p.returncode), file=sys.stderr)
|
||||
if stderr:
|
||||
print(stderr.rstrip(), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def die(message):
|
||||
print("error:", message, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def to_bytes(str_input):
|
||||
# Encode to UTF-8 to get binary data.
|
||||
if isinstance(str_input, bytes):
|
||||
return str_input
|
||||
return str_input.encode("utf-8")
|
||||
|
||||
|
||||
def to_string(bytes_input):
|
||||
if isinstance(bytes_input, str):
|
||||
return bytes_input
|
||||
return bytes_input.encode("utf-8")
|
||||
|
||||
|
||||
def convert_string(bytes_input):
|
||||
try:
|
||||
return to_string(bytes_input.decode("utf-8"))
|
||||
except AttributeError: # 'str' object has no attribute 'decode'.
|
||||
return str(bytes_input)
|
||||
except UnicodeError:
|
||||
return str(bytes_input)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
155
frontend/src/common/prettier/plugins/clang/index.ts
Normal file
155
frontend/src/common/prettier/plugins/clang/index.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Prettier Plugin for C/C++ formatting using clang-format WebAssembly
|
||||
*
|
||||
* This plugin provides support for formatting C/C++ files using the clang-format WASM implementation.
|
||||
* It supports various C/C++ file extensions and common clang-format styles.
|
||||
*/
|
||||
import type { Plugin, Parser, Printer } from 'prettier';
|
||||
|
||||
// Import the clang-format WASM module
|
||||
import clangFormatInit, { format } from './clang-format-vite.js';
|
||||
|
||||
const parserName = 'clang';
|
||||
|
||||
// Language configuration
|
||||
const languages = [
|
||||
{
|
||||
name: 'C',
|
||||
aliases: ['c'],
|
||||
parsers: [parserName],
|
||||
extensions: ['.c', '.h'],
|
||||
aceMode: 'c_cpp',
|
||||
tmScope: 'source.c',
|
||||
linguistLanguageId: 50,
|
||||
vscodeLanguageIds: ['c']
|
||||
},
|
||||
{
|
||||
name: 'C++',
|
||||
aliases: ['cpp', 'cxx', 'cc'],
|
||||
parsers: [parserName],
|
||||
extensions: ['.cpp', '.cxx', '.cc', '.hpp', '.hxx', '.hh', '.C', '.H'],
|
||||
aceMode: 'c_cpp',
|
||||
tmScope: 'source.cpp',
|
||||
linguistLanguageId: 43,
|
||||
vscodeLanguageIds: ['cpp']
|
||||
},
|
||||
{
|
||||
name: 'Objective-C',
|
||||
aliases: ['objc', 'objectivec'],
|
||||
parsers: [parserName],
|
||||
extensions: ['.m', '.mm'],
|
||||
aceMode: 'objectivec',
|
||||
tmScope: 'source.objc',
|
||||
linguistLanguageId: 259,
|
||||
vscodeLanguageIds: ['objective-c']
|
||||
}
|
||||
];
|
||||
|
||||
// Parser configuration
|
||||
const clangParser: Parser<string> = {
|
||||
astFormat: parserName,
|
||||
parse: (text: string) => text,
|
||||
locStart: () => 0,
|
||||
locEnd: (node: string) => node.length,
|
||||
};
|
||||
|
||||
// Initialize clang-format WASM module
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let isInitialized = false;
|
||||
|
||||
function initClangFormat(): Promise<void> {
|
||||
if (initPromise) {
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
if (!isInitialized) {
|
||||
await clangFormatInit();
|
||||
isInitialized = true;
|
||||
}
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
// Printer configuration
|
||||
const clangPrinter: Printer<string> = {
|
||||
print: (path, options) => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
console.warn('clang-format WASM module not initialized, returning original text');
|
||||
return (path as any).getValue ? (path as any).getValue() : path.node;
|
||||
}
|
||||
|
||||
const text = (path as any).getValue ? (path as any).getValue() : path.node;
|
||||
const style = getClangStyle(options);
|
||||
|
||||
// Format using clang-format (synchronous call)
|
||||
const formatted = format(text, undefined, style);
|
||||
|
||||
return formatted.trim();
|
||||
} catch (error) {
|
||||
console.warn('clang-format failed:', error);
|
||||
// Return original text if formatting fails
|
||||
return (path as any).getValue ? (path as any).getValue() : path.node;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Helper function to determine clang-format style
|
||||
function getClangStyle(options: any): string {
|
||||
// You can extend this to support more options
|
||||
const style = options.clangStyle || 'LLVM';
|
||||
|
||||
// Support common styles
|
||||
const validStyles = ['LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit', 'Microsoft', 'GNU'];
|
||||
if (validStyles.includes(style)) {
|
||||
return style;
|
||||
}
|
||||
|
||||
// Default to LLVM style
|
||||
return 'LLVM';
|
||||
}
|
||||
|
||||
// Plugin options
|
||||
const options = {
|
||||
clangStyle: {
|
||||
since: '0.0.1',
|
||||
category: 'Format' as const,
|
||||
type: 'choice' as const,
|
||||
default: 'LLVM',
|
||||
description: 'The clang-format style to use',
|
||||
choices: [
|
||||
{ value: 'LLVM', description: 'LLVM coding standards' },
|
||||
{ value: 'Google', description: "Google's C++ style guide" },
|
||||
{ value: 'Chromium', description: "Chromium's style guide" },
|
||||
{ value: 'Mozilla', description: "Mozilla's style guide" },
|
||||
{ value: 'WebKit', description: "WebKit's style guide" },
|
||||
{ value: 'Microsoft', description: "Microsoft's style guide" },
|
||||
{ value: 'GNU', description: 'GNU coding standards' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Plugin object
|
||||
const clangPlugin: Plugin = {
|
||||
languages,
|
||||
parsers: {
|
||||
[parserName]: clangParser,
|
||||
},
|
||||
printers: {
|
||||
[parserName]: clangPrinter,
|
||||
},
|
||||
options,
|
||||
};
|
||||
|
||||
// Initialize WASM module when plugin loads
|
||||
initClangFormat().catch(error => {
|
||||
console.warn('Failed to initialize clang-format WASM module:', error);
|
||||
});
|
||||
|
||||
export default clangPlugin;
|
||||
export { languages };
|
||||
export const parsers = clangPlugin.parsers;
|
||||
export const printers = clangPlugin.printers;
|
||||
323
frontend/src/common/prettier/plugins/clang/lib.cc
Normal file
323
frontend/src/common/prettier/plugins/clang/lib.cc
Normal file
@@ -0,0 +1,323 @@
|
||||
//===-- clang-format/ClangFormat.cpp - Clang format tool ------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// \file
|
||||
/// This file implements a clang-format tool that automatically formats
|
||||
/// (fragments of) C++ code.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "lib.h"
|
||||
#include "clang/Basic/FileManager.h"
|
||||
#include "clang/Basic/SourceManager.h"
|
||||
#include "clang/Basic/Version.h"
|
||||
#include "clang/Format/Format.h"
|
||||
#include "clang/Rewrite/Core/Rewriter.h"
|
||||
|
||||
using namespace llvm;
|
||||
using clang::tooling::Replacements;
|
||||
|
||||
static std::string FallbackStyle{clang::format::DefaultFallbackStyle};
|
||||
|
||||
static unsigned Cursor{0};
|
||||
|
||||
static bool SortIncludes{false};
|
||||
|
||||
static std::string QualifierAlignment{""};
|
||||
|
||||
static auto Ok(const std::string content) -> Result {
|
||||
return {false, std::move(content)};
|
||||
}
|
||||
|
||||
static auto Err(const std::string content) -> Result {
|
||||
return {true, std::move(content)};
|
||||
}
|
||||
|
||||
namespace clang {
|
||||
namespace format {
|
||||
|
||||
static FileID createInMemoryFile(StringRef FileName, MemoryBufferRef Source,
|
||||
SourceManager &Sources, FileManager &Files,
|
||||
llvm::vfs::InMemoryFileSystem *MemFS) {
|
||||
MemFS->addFileNoOwn(FileName, 0, Source);
|
||||
auto File = Files.getOptionalFileRef(FileName);
|
||||
assert(File && "File not added to MemFS?");
|
||||
return Sources.createFileID(*File, SourceLocation(), SrcMgr::C_User);
|
||||
}
|
||||
|
||||
static auto fillRanges(MemoryBuffer *Code, std::vector<tooling::Range> &Ranges)
|
||||
-> void {
|
||||
Ranges.push_back(tooling::Range(0, Code->getBuffer().size()));
|
||||
}
|
||||
|
||||
static auto isPredefinedStyle(StringRef style) -> bool {
|
||||
return StringSwitch<bool>(style.lower())
|
||||
.Cases("llvm", "chromium", "mozilla", "google", "webkit", "gnu",
|
||||
"microsoft", "none", "file", true)
|
||||
.Default(false);
|
||||
}
|
||||
|
||||
static auto format_range(const std::unique_ptr<llvm::MemoryBuffer> code,
|
||||
const std::string assumedFileName,
|
||||
const std::string style,
|
||||
std::vector<tooling::Range> ranges) -> Result {
|
||||
StringRef BufStr = code->getBuffer();
|
||||
|
||||
const char *InvalidBOM = SrcMgr::ContentCache::getInvalidBOM(BufStr);
|
||||
|
||||
if (InvalidBOM) {
|
||||
std::stringstream err;
|
||||
err << "encoding with unsupported byte order mark \"" << InvalidBOM
|
||||
<< "\" detected.";
|
||||
|
||||
return Err(err.str());
|
||||
}
|
||||
|
||||
StringRef AssumedFileName = assumedFileName;
|
||||
if (AssumedFileName.empty())
|
||||
AssumedFileName = "<stdin>";
|
||||
|
||||
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
|
||||
new llvm::vfs::InMemoryFileSystem);
|
||||
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
|
||||
|
||||
DiagnosticOptions DiagOpts;
|
||||
DiagnosticsEngine Diagnostics(
|
||||
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
|
||||
SourceManager Sources(Diagnostics, Files);
|
||||
|
||||
StringRef _style = style;
|
||||
|
||||
if (!_style.starts_with("{") && !isPredefinedStyle(_style)) {
|
||||
std::unique_ptr<llvm::MemoryBuffer> DotClangFormat =
|
||||
MemoryBuffer::getMemBuffer(style);
|
||||
|
||||
createInMemoryFile(".clang-format", *DotClangFormat.get(), Sources, Files,
|
||||
InMemoryFileSystem.get());
|
||||
_style = "file:.clang-format";
|
||||
}
|
||||
|
||||
llvm::Expected<FormatStyle> FormatStyle =
|
||||
getStyle(_style, AssumedFileName, FallbackStyle, code->getBuffer(),
|
||||
InMemoryFileSystem.get(), false);
|
||||
|
||||
InMemoryFileSystem.reset();
|
||||
|
||||
if (!FormatStyle) {
|
||||
std::string err = llvm::toString(FormatStyle.takeError());
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
StringRef QualifierAlignmentOrder = QualifierAlignment;
|
||||
|
||||
FormatStyle->QualifierAlignment =
|
||||
StringSwitch<FormatStyle::QualifierAlignmentStyle>(
|
||||
QualifierAlignmentOrder.lower())
|
||||
.Case("right", FormatStyle::QAS_Right)
|
||||
.Case("left", FormatStyle::QAS_Left)
|
||||
.Default(FormatStyle->QualifierAlignment);
|
||||
|
||||
if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Left) {
|
||||
FormatStyle->QualifierOrder = {"const", "volatile", "type"};
|
||||
} else if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Right) {
|
||||
FormatStyle->QualifierOrder = {"type", "const", "volatile"};
|
||||
} else if (QualifierAlignmentOrder.contains("type")) {
|
||||
FormatStyle->QualifierAlignment = FormatStyle::QAS_Custom;
|
||||
SmallVector<StringRef> Qualifiers;
|
||||
QualifierAlignmentOrder.split(Qualifiers, " ", /*MaxSplit=*/-1,
|
||||
/*KeepEmpty=*/false);
|
||||
FormatStyle->QualifierOrder = {Qualifiers.begin(), Qualifiers.end()};
|
||||
}
|
||||
|
||||
if (SortIncludes) {
|
||||
FormatStyle->SortIncludes = {};
|
||||
FormatStyle->SortIncludes.Enabled = true;
|
||||
}
|
||||
|
||||
unsigned CursorPosition = Cursor;
|
||||
Replacements Replaces = sortIncludes(*FormatStyle, code->getBuffer(), ranges,
|
||||
AssumedFileName, &CursorPosition);
|
||||
|
||||
// To format JSON insert a variable to trick the code into thinking its
|
||||
// JavaScript.
|
||||
if (FormatStyle->isJson() && !FormatStyle->DisableFormat) {
|
||||
auto err =
|
||||
Replaces.add(tooling::Replacement(AssumedFileName, 0, 0, "x = "));
|
||||
if (err)
|
||||
return Err("Bad Json variable insertion");
|
||||
}
|
||||
|
||||
auto ChangedCode =
|
||||
cantFail(tooling::applyAllReplacements(code->getBuffer(), Replaces));
|
||||
|
||||
// Get new affected ranges after sorting `#includes`.
|
||||
ranges = tooling::calculateRangesAfterReplacements(Replaces, ranges);
|
||||
FormattingAttemptStatus Status;
|
||||
Replacements FormatChanges =
|
||||
reformat(*FormatStyle, ChangedCode, ranges, AssumedFileName, &Status);
|
||||
Replaces = Replaces.merge(FormatChanges);
|
||||
|
||||
return Ok(
|
||||
cantFail(tooling::applyAllReplacements(code->getBuffer(), Replaces)));
|
||||
}
|
||||
|
||||
static auto format_range(const std::string str,
|
||||
const std::string assumedFileName,
|
||||
const std::string style, const bool is_line_range,
|
||||
const std::vector<unsigned> ranges) -> Result {
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
|
||||
MemoryBuffer::getMemBuffer(str);
|
||||
|
||||
if (std::error_code EC = CodeOrErr.getError())
|
||||
return Err(EC.message());
|
||||
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
|
||||
if (Code->getBufferSize() == 0)
|
||||
return Ok(""); // Empty files are formatted correctly.
|
||||
|
||||
std::vector<tooling::Range> Ranges;
|
||||
|
||||
if (ranges.empty()) {
|
||||
fillRanges(Code.get(), Ranges);
|
||||
return format_range(std::move(Code), assumedFileName, style,
|
||||
std::move(Ranges));
|
||||
}
|
||||
|
||||
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
|
||||
new llvm::vfs::InMemoryFileSystem);
|
||||
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
|
||||
DiagnosticOptions DiagOpts;
|
||||
DiagnosticsEngine Diagnostics(
|
||||
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
|
||||
SourceManager Sources(Diagnostics, Files);
|
||||
FileID ID = createInMemoryFile("<irrelevant>", *Code, Sources, Files,
|
||||
InMemoryFileSystem.get());
|
||||
|
||||
if (is_line_range) {
|
||||
for (auto FromLine = begin(ranges); FromLine < end(ranges); FromLine += 2) {
|
||||
auto ToLine = FromLine + 1;
|
||||
|
||||
SourceLocation Start = Sources.translateLineCol(ID, *FromLine, 1);
|
||||
SourceLocation End = Sources.translateLineCol(ID, *ToLine, UINT_MAX);
|
||||
if (Start.isInvalid() || End.isInvalid())
|
||||
return Err("invalid line number");
|
||||
unsigned Offset = Sources.getFileOffset(Start);
|
||||
unsigned Length = Sources.getFileOffset(End) - Offset;
|
||||
Ranges.push_back(tooling::Range(Offset, Length));
|
||||
}
|
||||
} else {
|
||||
if (ranges.size() > 2 && ranges.size() % 2 != 0)
|
||||
return Err("number of -offset and -length arguments must match");
|
||||
|
||||
if (ranges.size() == 1) {
|
||||
auto offset = begin(ranges);
|
||||
if (*offset >= Code->getBufferSize()) {
|
||||
std::stringstream err;
|
||||
err << "offset " << *offset << " is outside the file";
|
||||
return Err(err.str());
|
||||
}
|
||||
SourceLocation Start =
|
||||
Sources.getLocForStartOfFile(ID).getLocWithOffset(*offset);
|
||||
SourceLocation End = Sources.getLocForEndOfFile(ID);
|
||||
|
||||
unsigned Offset = Sources.getFileOffset(Start);
|
||||
unsigned Length = Sources.getFileOffset(End) - Offset;
|
||||
|
||||
Ranges.push_back(tooling::Range(Offset, Length));
|
||||
} else {
|
||||
for (auto offset = begin(ranges); offset < end(ranges); offset += 2) {
|
||||
auto length = offset + 1;
|
||||
|
||||
if (*offset >= Code->getBufferSize()) {
|
||||
std::stringstream err;
|
||||
err << "offset " << *offset << " is outside the file";
|
||||
return Err(err.str());
|
||||
}
|
||||
|
||||
unsigned end = *offset + *length;
|
||||
if (end > Code->getBufferSize()) {
|
||||
std::stringstream err;
|
||||
err << "invalid length " << *length << ", offset + length (" << end
|
||||
<< ") is outside the file.";
|
||||
return Err(err.str());
|
||||
}
|
||||
|
||||
SourceLocation Start =
|
||||
Sources.getLocForStartOfFile(ID).getLocWithOffset(*offset);
|
||||
SourceLocation End = Start.getLocWithOffset(*length);
|
||||
|
||||
unsigned Offset = Sources.getFileOffset(Start);
|
||||
unsigned Length = Sources.getFileOffset(End) - Offset;
|
||||
|
||||
Ranges.push_back(tooling::Range(Offset, Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return format_range(std::move(Code), assumedFileName, style,
|
||||
std::move(Ranges));
|
||||
}
|
||||
|
||||
static auto format(const std::string str, const std::string assumedFileName,
|
||||
const std::string style) -> Result {
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
|
||||
MemoryBuffer::getMemBuffer(str);
|
||||
|
||||
if (std::error_code EC = CodeOrErr.getError())
|
||||
return Err(EC.message());
|
||||
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
|
||||
if (Code->getBufferSize() == 0)
|
||||
return Ok(""); // Empty files are formatted correctly.
|
||||
|
||||
std::vector<tooling::Range> Ranges;
|
||||
fillRanges(Code.get(), Ranges);
|
||||
|
||||
return format_range(std::move(Code), assumedFileName, style,
|
||||
std::move(Ranges));
|
||||
}
|
||||
|
||||
} // namespace format
|
||||
} // namespace clang
|
||||
|
||||
auto version() -> std::string {
|
||||
return clang::getClangToolFullVersion("clang-format");
|
||||
}
|
||||
|
||||
auto format(const std::string str, const std::string assumedFileName,
|
||||
const std::string style) -> Result {
|
||||
return clang::format::format(str, assumedFileName, style);
|
||||
}
|
||||
|
||||
auto format_byte(const std::string str, const std::string assumedFileName,
|
||||
const std::string style, const std::vector<unsigned> ranges)
|
||||
-> Result {
|
||||
return clang::format::format_range(str, assumedFileName, style, false,
|
||||
std::move(ranges));
|
||||
}
|
||||
|
||||
auto format_line(const std::string str, const std::string assumedFileName,
|
||||
const std::string style, const std::vector<unsigned> ranges)
|
||||
-> Result {
|
||||
return clang::format::format_range(str, assumedFileName, style, true,
|
||||
std::move(ranges));
|
||||
}
|
||||
|
||||
auto set_fallback_style(const std::string style) -> void {
|
||||
FallbackStyle = style;
|
||||
}
|
||||
|
||||
auto set_sort_includes(const bool sort) -> void { SortIncludes = sort; }
|
||||
|
||||
auto dump_config(const std::string style, const std::string FileName,
|
||||
const std::string code) -> Result {
|
||||
llvm::Expected<clang::format::FormatStyle> FormatStyle =
|
||||
clang::format::getStyle(style, FileName, FallbackStyle, code);
|
||||
if (!FormatStyle)
|
||||
return Err(llvm::toString(FormatStyle.takeError()));
|
||||
std::string Config = clang::format::configurationAsText(*FormatStyle);
|
||||
return Ok(Config);
|
||||
}
|
||||
24
frontend/src/common/prettier/plugins/clang/lib.h
Normal file
24
frontend/src/common/prettier/plugins/clang/lib.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef CLANG_FORMAT_WASM_LIB_H_
|
||||
#define CLANG_FORMAT_WASM_LIB_H_
|
||||
#include <sstream>
|
||||
|
||||
struct Result {
|
||||
bool error;
|
||||
std::string content;
|
||||
};
|
||||
|
||||
auto version() -> std::string;
|
||||
auto format(const std::string str, const std::string assumedFileName, const std::string style) -> Result;
|
||||
auto format_byte(const std::string str,
|
||||
const std::string assumedFileName,
|
||||
const std::string style,
|
||||
const std::vector<unsigned> ranges) -> Result;
|
||||
auto format_line(const std::string str,
|
||||
const std::string assumedFileName,
|
||||
const std::string style,
|
||||
const std::vector<unsigned> ranges) -> Result;
|
||||
auto set_fallback_style(const std::string style) -> void;
|
||||
auto set_sort_includes(const bool sort) -> void;
|
||||
auto dump_config(const std::string style, const std::string FileName, const std::string code) -> Result;
|
||||
|
||||
#endif
|
||||
146
frontend/src/common/prettier/plugins/clang/template.js
Normal file
146
frontend/src/common/prettier/plugins/clang/template.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/* @ts-self-types="./clang-format.d.ts" */
|
||||
async function load(module) {
|
||||
if (typeof Response === "function" && module instanceof Response) {
|
||||
if ("compileStreaming" in WebAssembly) {
|
||||
try {
|
||||
return await WebAssembly.compileStreaming(module);
|
||||
} catch (e) {
|
||||
if (module.headers.get("Content-Type") !== "application/wasm") {
|
||||
console.warn(
|
||||
"`WebAssembly.compileStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
|
||||
e,
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return module.arrayBuffer();
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
let wasm;
|
||||
export default async function initAsync(input) {
|
||||
if (wasm !== undefined) {
|
||||
return wasm;
|
||||
}
|
||||
|
||||
if (typeof input === "undefined") {
|
||||
input = new URL("clang-format.wasm", import.meta.url);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === "string" ||
|
||||
(typeof Request === "function" && input instanceof Request) ||
|
||||
(typeof URL === "function" && input instanceof URL)
|
||||
) {
|
||||
input = fetch(input);
|
||||
}
|
||||
|
||||
wasm = await load(await input).then((wasm) => Module({ wasm }));
|
||||
assert_init = () => {};
|
||||
}
|
||||
|
||||
function assert_init() {
|
||||
throw new Error("uninit");
|
||||
}
|
||||
|
||||
export function version() {
|
||||
assert_init();
|
||||
return wasm.version();
|
||||
}
|
||||
|
||||
export function set_fallback_style(style) {
|
||||
assert_init();
|
||||
wasm.set_fallback_style(style);
|
||||
}
|
||||
|
||||
export function set_sort_includes(sort) {
|
||||
assert_init();
|
||||
wasm.set_sort_includes(sort);
|
||||
}
|
||||
|
||||
function unwrap(result) {
|
||||
const { error, content } = result;
|
||||
if (error) {
|
||||
throw Error(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function format(content, filename = "<stdin>", style = "LLVM") {
|
||||
assert_init();
|
||||
const result = wasm.format(content, filename, style);
|
||||
return unwrap(result);
|
||||
}
|
||||
|
||||
export function format_line_range(
|
||||
content,
|
||||
range,
|
||||
filename = "<stdin>",
|
||||
style = "LLVM",
|
||||
) {
|
||||
assert_init();
|
||||
const rangeList = new wasm.RangeList();
|
||||
for (const [fromLine, toLine] of range) {
|
||||
if (fromLine < 1) {
|
||||
throw Error("start line should be at least 1");
|
||||
}
|
||||
if (fromLine > toLine) {
|
||||
throw Error("start line should not exceed end line");
|
||||
}
|
||||
rangeList.push_back(fromLine);
|
||||
rangeList.push_back(toLine);
|
||||
}
|
||||
|
||||
const result = wasm.format_line(content, filename, style, rangeList);
|
||||
rangeList.delete();
|
||||
return unwrap(result);
|
||||
}
|
||||
|
||||
export function format_byte_range(
|
||||
content,
|
||||
range,
|
||||
filename = "<stdin>",
|
||||
style = "LLVM",
|
||||
) {
|
||||
assert_init();
|
||||
const rangeList = new wasm.RangeList();
|
||||
|
||||
if (range.length === 1 && range[0].length === 1) {
|
||||
rangeList.push_back(range[0][0]);
|
||||
} else {
|
||||
for (const [offset, length] of range) {
|
||||
if (offset < 0) {
|
||||
throw Error("start offset should be at least 0");
|
||||
}
|
||||
if (length < 0) {
|
||||
throw Error("length should be at least 0");
|
||||
}
|
||||
rangeList.push_back(offset);
|
||||
rangeList.push_back(length);
|
||||
}
|
||||
}
|
||||
|
||||
const result = wasm.format_byte(content, filename, style, rangeList);
|
||||
rangeList.delete();
|
||||
return unwrap(result);
|
||||
}
|
||||
|
||||
export function dump_config({
|
||||
style = "file",
|
||||
filename = "<stdin>",
|
||||
code = "",
|
||||
} = {}) {
|
||||
assert_init();
|
||||
const result = wasm.dump_config(style, filename, code);
|
||||
return unwrap(result);
|
||||
}
|
||||
|
||||
export {
|
||||
format_byte_range as formatByteRange,
|
||||
format_line_range as formatLineRange,
|
||||
};
|
||||
42
frontend/src/common/prettier/plugins/go/build-tinygo.bat
Normal file
42
frontend/src/common/prettier/plugins/go/build-tinygo.bat
Normal file
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
rem Build script for Go Prettier Plugin WASM using TinyGo
|
||||
rem This script compiles the Go code to WebAssembly for browser environment
|
||||
|
||||
echo Building Go Prettier Plugin WASM with TinyGo...
|
||||
|
||||
rem Check if TinyGo is available
|
||||
tinygo version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo TinyGo not found! Please install TinyGo first.
|
||||
echo Visit: https://tinygo.org/getting-started/install/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rem Display TinyGo version
|
||||
echo Using TinyGo version:
|
||||
tinygo version
|
||||
|
||||
rem Build the WASM file using TinyGo
|
||||
echo Compiling main.go to go.wasm with TinyGo...
|
||||
tinygo build -o go-format.wasm -target wasm main.go
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build successful!
|
||||
|
||||
rem Show file size (Windows version)
|
||||
for %%A in (go.wasm) do echo WASM file size: %%~zA bytes
|
||||
|
||||
rem Copy to public directory for browser access
|
||||
if exist "..\..\..\..\..\public" (
|
||||
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
|
||||
echo Copied to public directory
|
||||
del go.wasm
|
||||
echo Cleaned up local WASM file
|
||||
)
|
||||
|
||||
echo Go Prettier Plugin WASM (TinyGo) is ready!
|
||||
38
frontend/src/common/prettier/plugins/go/build-tinygo.sh
Normal file
38
frontend/src/common/prettier/plugins/go/build-tinygo.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Go Prettier Plugin WASM using TinyGo
|
||||
# This script compiles the Go code to WebAssembly for browser environment
|
||||
|
||||
echo "Building Go Prettier Plugin WASM with TinyGo..."
|
||||
|
||||
# Check if TinyGo is available
|
||||
if ! command -v tinygo &> /dev/null; then
|
||||
echo "TinyGo not found! Please install TinyGo first."
|
||||
echo "Visit: https://tinygo.org/getting-started/install/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display TinyGo version
|
||||
echo "Using TinyGo version: $(tinygo version)"
|
||||
|
||||
# Build the WASM file using TinyGo
|
||||
echo "Compiling main.go to go.wasm with TinyGo..."
|
||||
tinygo build -o go-format.wasm -target wasm main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful!"
|
||||
echo "WASM file size: $(du -h go-format.wasm | cut -f1)"
|
||||
|
||||
# Copy to public directory for browser access
|
||||
if [ -d "../../../../../public" ]; then
|
||||
cp go-format.wasm ../../../../../public/go-format.wasm
|
||||
echo "Copied to public directory"
|
||||
rm go-format.wasm
|
||||
echo "Cleaned up local WASM file"
|
||||
fi
|
||||
|
||||
echo "Go Prettier Plugin WASM (TinyGo) is ready!"
|
||||
@@ -1,32 +1,43 @@
|
||||
@echo off
|
||||
rem Build script for Go Prettier Plugin WASM
|
||||
rem This script compiles the Go code to WebAssembly
|
||||
rem Build script for Go Prettier Plugin WASM using native Go
|
||||
rem This script compiles the Go code to WebAssembly for browser environment
|
||||
|
||||
echo 🔨 Building Go Prettier Plugin WASM...
|
||||
echo Building Go Prettier Plugin WASM with native Go...
|
||||
|
||||
rem Set WASM build environment
|
||||
rem Check if Go is available
|
||||
go version >nul 2>&1
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo Go not found! Please install Go 1.21+ first.
|
||||
echo Visit: https://golang.org/dl/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rem Set WASM build environment for browser (js/wasm)
|
||||
set GOOS=js
|
||||
set GOARCH=wasm
|
||||
|
||||
rem Build the WASM file
|
||||
echo Compiling main.go to go.wasm...
|
||||
go build -o go.wasm main.go
|
||||
rem Build the WASM file using native Go
|
||||
echo Compiling main.go to go.wasm with Go...
|
||||
go build -o go-format.wasm main.go
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo ✅ Build successful!
|
||||
echo Build successful!
|
||||
|
||||
rem Show file size (Windows version)
|
||||
for %%A in (go.wasm) do echo 📊 WASM file size: %%~zA bytes
|
||||
for %%A in (go.wasm) do echo WASM file size: %%~zA bytes
|
||||
|
||||
rem Copy to public directory for browser access
|
||||
if exist "..\..\..\..\..\public" (
|
||||
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
|
||||
echo 📋 Copied to public directory
|
||||
echo Copied to public directory
|
||||
del go.wasm
|
||||
echo Cleaned up local WASM file
|
||||
)
|
||||
|
||||
echo 🎉 Go Prettier Plugin WASM is ready!
|
||||
echo Go Prettier Plugin WASM is ready!
|
||||
) else (
|
||||
echo ❌ Build failed!
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
@@ -1,30 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Go Prettier Plugin WASM
|
||||
# This script compiles the Go code to WebAssembly
|
||||
# Build script for Go Prettier Plugin WASM using native Go
|
||||
# This script compiles the Go code to WebAssembly for browser environment
|
||||
|
||||
echo "🔨 Building Go Prettier Plugin WASM..."
|
||||
echo "Building Go Prettier Plugin WASM with native Go..."
|
||||
|
||||
# Set WASM build environment
|
||||
# Check if Go is available
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo "Go not found! Please install Go 1.21+ first."
|
||||
echo "Visit: https://golang.org/dl/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display Go version
|
||||
echo "Using Go version: $(go version)"
|
||||
|
||||
# Set WASM build environment for browser (js/wasm)
|
||||
export GOOS=js
|
||||
export GOARCH=wasm
|
||||
|
||||
# Build the WASM file
|
||||
echo "Compiling main.go to go.wasm..."
|
||||
go build -o go.wasm main.go
|
||||
# Build the WASM file using native Go
|
||||
echo "Compiling main.go to go.wasm with Go..."
|
||||
go build -o go-format.wasm main.go
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Build successful!"
|
||||
echo "📊 WASM file size: $(du -h go.wasm | cut -f1)"
|
||||
echo "Build successful!"
|
||||
echo "WASM file size: $(du -h go-format.wasm | cut -f1)"
|
||||
|
||||
# Copy to public directory for browser access
|
||||
if [ -d "../../../../../public" ]; then
|
||||
cp go.wasm ../../../../../public/go.wasm
|
||||
echo "📋 Copied to public directory"
|
||||
cp go-format.wasm ../../../../../public/go-format.wasm
|
||||
echo "Copied to public directory"
|
||||
rm go-format.wasm
|
||||
echo "Cleaned up local WASM file"
|
||||
fi
|
||||
|
||||
echo "🎉 Go Prettier Plugin WASM is ready!"
|
||||
echo "Go Prettier Plugin WASM is ready!"
|
||||
else
|
||||
echo "❌ Build failed!"
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -1,244 +1,142 @@
|
||||
/**
|
||||
* Go Prettier Plugin - Universal Implementation
|
||||
* WebAssembly-based Go code formatter for Prettier
|
||||
* Supports both Node.js and Browser environments
|
||||
* @fileoverview Go Prettier Format Plugin
|
||||
* A Prettier plugin for formatting Go code using WebAssembly.
|
||||
* This plugin leverages Go's native formatting capabilities through WASM.
|
||||
*/
|
||||
import "./wasm_exec.js"
|
||||
/** @type {Promise<void>|null} */
|
||||
let initializePromise;
|
||||
|
||||
let initializePromise = null;
|
||||
|
||||
// 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) {
|
||||
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.arrayBuffer();
|
||||
} catch (error) {
|
||||
console.error('Browser WASM loading failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Node.js Go runtime initialization
|
||||
const initGoRuntimeNode = async () => {
|
||||
if (globalThis.Go) return;
|
||||
|
||||
try {
|
||||
// Dynamic import of wasm_exec.js for Node.js
|
||||
const { createRequire } = await import('module');
|
||||
const require = createRequire(import.meta.url);
|
||||
const path = await import('path');
|
||||
const { fileURLToPath } = await import('url');
|
||||
|
||||
const __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 not available after loading wasm_exec.js');
|
||||
}
|
||||
} catch (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 () => {
|
||||
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');
|
||||
})();
|
||||
|
||||
/**
|
||||
* Initializes the Go WebAssembly module for formatting Go code.
|
||||
* This function sets up the WASM runtime and makes the formatGo function
|
||||
* available on the global object.
|
||||
*
|
||||
* @async
|
||||
* @function initialize
|
||||
* @returns {Promise<void>} A promise that resolves when the WASM module is ready
|
||||
* @throws {Error} If the WASM file cannot be loaded or instantiated
|
||||
*/
|
||||
function initialize() {
|
||||
if (initializePromise) {
|
||||
return initializePromise;
|
||||
};
|
||||
}
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
name: "Go",
|
||||
parsers: ["go-format"],
|
||||
extensions: [".go"],
|
||||
vscodeLanguageIds: ["go"],
|
||||
},
|
||||
initializePromise = (async () => {
|
||||
|
||||
const go = new TinyGo();
|
||||
|
||||
// Load WASM file from browser
|
||||
const response = await fetch('/go-format.wasm');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const wasmBuffer = await response.arrayBuffer();
|
||||
|
||||
const { instance } = await WebAssembly.instantiate(
|
||||
wasmBuffer,
|
||||
go.importObject
|
||||
);
|
||||
|
||||
// go.run returns a promise that resolves when the go program exits.
|
||||
// Since our program is a long-running service (it exposes a function and waits),
|
||||
// we don't await it.
|
||||
go.run(instance);
|
||||
|
||||
// The `formatGo` function is now available on the global object.
|
||||
})();
|
||||
|
||||
return initializePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prettier language configuration for Go.
|
||||
* Defines the language settings, file extensions, and parser mappings.
|
||||
*
|
||||
* @type {Array<Object>}
|
||||
* @property {string} name - The display name of the language
|
||||
* @property {string[]} parsers - Array of parser names for this language
|
||||
* @property {string[]} extensions - File extensions associated with this language
|
||||
* @property {string[]} vscodeLanguageIds - VSCode language identifier mappings
|
||||
*/
|
||||
const languages = [
|
||||
{
|
||||
name: "Go",
|
||||
parsers: ["go-format"],
|
||||
extensions: [".go"],
|
||||
vscodeLanguageIds: ["go"],
|
||||
},
|
||||
];
|
||||
|
||||
export const parsers = {
|
||||
"go-format": {
|
||||
parse: (text) => text,
|
||||
astFormat: "go-format",
|
||||
locStart: (node) => 0,
|
||||
locEnd: (node) => node.length,
|
||||
/**
|
||||
* Prettier parser configuration for Go.
|
||||
* Defines how Go source code should be parsed and processed.
|
||||
*
|
||||
* @type {Object<string, Object>}
|
||||
* @property {Object} go-format - Go language parser configuration
|
||||
* @property {Function} go-format.parse - Parser function that returns the input text as-is
|
||||
* @property {string} go-format.astFormat - AST format identifier for the printer
|
||||
* @property {Function} go-format.locStart - Function to get the start location of a node
|
||||
* @property {Function} go-format.locEnd - Function to get the end location of a node
|
||||
*/
|
||||
const parsers = {
|
||||
"go-format": {
|
||||
/**
|
||||
* Parse Go source code. For this plugin, we pass through the text as-is
|
||||
* since the actual formatting is handled by the Go WASM module.
|
||||
*
|
||||
* @param {string} text - The Go source code to parse
|
||||
* @returns {string} The input text unchanged
|
||||
*/
|
||||
parse: (text) => text,
|
||||
astFormat: "go-format",
|
||||
// These are required for Prettier to work
|
||||
/**
|
||||
* Get the start location of a node in the source code.
|
||||
*
|
||||
* @param {string} node - The node (in this case, the source text)
|
||||
* @returns {number} Always returns 0 as we treat the entire text as one node
|
||||
*/
|
||||
locStart: (node) => 0,
|
||||
/**
|
||||
* Get the end location of a node in the source code.
|
||||
*
|
||||
* @param {string} node - The node (in this case, the source text)
|
||||
* @returns {number} The length of the text
|
||||
*/
|
||||
locEnd: (node) => node.length,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Prettier printer configuration for Go.
|
||||
* Defines how the parsed Go AST should be formatted back to text.
|
||||
*
|
||||
* @type {Object<string, Object>}
|
||||
* @property {Object} go-format - Go formatting printer configuration
|
||||
* @property {Function} go-format.print - Async function that formats Go code
|
||||
*/
|
||||
const printers = {
|
||||
"go-format": {
|
||||
/**
|
||||
* Format Go source code using the WebAssembly Go formatter.
|
||||
* This function initializes the WASM module if needed and calls the
|
||||
* global formatGo function exposed by the Go program.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} path - Prettier's path object containing the source code
|
||||
* @param {Function} path.getValue - Function to get the current node value
|
||||
* @returns {Promise<string>} The formatted Go source code
|
||||
* @throws {Error} If the WASM module fails to initialize or format the code
|
||||
*/
|
||||
print: async (path) => {
|
||||
// The WASM module must be initialized before we can format.
|
||||
await initialize();
|
||||
const text = path.getValue();
|
||||
// The `formatGo` function is exposed on the global object by our Go program.
|
||||
return globalThis.formatGo(text);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const printers = {
|
||||
"go-format": {
|
||||
print: (path) => {
|
||||
const text = path.getValue();
|
||||
|
||||
if (typeof globalThis.formatGo !== 'function') {
|
||||
// 如果 formatGo 函数不可用,尝试初始化
|
||||
initialize().then(() => {
|
||||
// 初始化完成后,formatGo 应该可用
|
||||
}).catch(err => {
|
||||
console.error('Go WASM initialization failed:', err);
|
||||
});
|
||||
|
||||
// 如果还是不可用,返回原始文本
|
||||
return text;
|
||||
}
|
||||
|
||||
try {
|
||||
return globalThis.formatGo(text);
|
||||
} catch (error) {
|
||||
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,
|
||||
parsers,
|
||||
printers
|
||||
};
|
||||
export default { languages, parsers, printers, initialize };
|
||||
Binary file not shown.
@@ -4,8 +4,8 @@
|
||||
// functionality for the Prettier plugin. This package exposes the formatGo function
|
||||
// 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.
|
||||
// The module is designed to be compiled to WebAssembly using native Go (GOOS=js GOARCH=wasm)
|
||||
// and loaded in browser environments as part of the Go Prettier plugin.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,26 +1,53 @@
|
||||
// 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";
|
||||
//
|
||||
// This file has been modified for use by the TinyGo compiler.
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
global.fs = require("node:fs");
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
if (!global.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
global.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);
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
@@ -58,8 +85,8 @@
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
@@ -73,58 +100,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("node:crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = require("node:util").TextEncoder;
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = require("node:util").TextDecoder;
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let reinterpretBuf = new DataView(new ArrayBuffer(8));
|
||||
var logLine = [];
|
||||
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
|
||||
|
||||
globalThis.Go = class {
|
||||
global.TinyGo = 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._callbackTimeouts = 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 mem = () => {
|
||||
// The buffer may change when requesting more memory.
|
||||
return new DataView(this._inst.exports.memory.buffer);
|
||||
}
|
||||
|
||||
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);
|
||||
const unboxValue = (v_ref) => {
|
||||
reinterpretBuf.setBigInt64(0, v_ref, true);
|
||||
const f = reinterpretBuf.getFloat64(0, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -132,69 +154,77 @@
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
const id = v_ref & 0xffffffffn;
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
const loadValue = (addr) => {
|
||||
let v_ref = mem().getBigUint64(addr, true);
|
||||
return unboxValue(v_ref);
|
||||
}
|
||||
|
||||
const boxValue = (v) => {
|
||||
const nanHead = 0x7FF80000n;
|
||||
|
||||
if (typeof v === "number") {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
return nanHead << 32n;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
if (v === 0) {
|
||||
return (nanHead << 32n) | 1n;
|
||||
}
|
||||
reinterpretBuf.setFloat64(0, v, true);
|
||||
return reinterpretBuf.getBigInt64(0, true);
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
switch (v) {
|
||||
case undefined:
|
||||
return 0n;
|
||||
case null:
|
||||
return (nanHead << 32n) | 2n;
|
||||
case true:
|
||||
return (nanHead << 32n) | 3n;
|
||||
case false:
|
||||
return (nanHead << 32n) | 4n;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
id = BigInt(this._values.length);
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
let typeFlag = 1n;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
typeFlag = 2n;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
typeFlag = 3n;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
typeFlag = 4n;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
return id | ((nanHead | typeFlag) << 32n);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
const storeValue = (addr, v) => {
|
||||
let v_ref = boxValue(v);
|
||||
mem().setBigUint64(addr, v_ref, true);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const loadSlice = (array, len, cap) => {
|
||||
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (array, len, cap) => {
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
@@ -202,347 +232,287 @@
|
||||
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 loadString = (ptr, len) => {
|
||||
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
wasi_snapshot_preview1: {
|
||||
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
|
||||
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
|
||||
let nwritten = 0;
|
||||
if (fd == 1) {
|
||||
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
|
||||
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
|
||||
let ptr = mem().getUint32(iov_ptr + 0, true);
|
||||
let len = mem().getUint32(iov_ptr + 4, true);
|
||||
nwritten += len;
|
||||
for (let i=0; i<len; i++) {
|
||||
let c = mem().getUint8(ptr+i);
|
||||
if (c == 13) { // CR
|
||||
// ignore
|
||||
} else if (c == 10) { // LF
|
||||
// write line
|
||||
let line = decoder.decode(new Uint8Array(logLine));
|
||||
logLine = [];
|
||||
console.log(line);
|
||||
} else {
|
||||
logLine.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('invalid file descriptor:', fd);
|
||||
}
|
||||
mem().setUint32(nwritten_ptr, nwritten, true);
|
||||
return 0;
|
||||
},
|
||||
fd_close: () => 0, // dummy
|
||||
fd_fdstat_get: () => 0, // dummy
|
||||
fd_seek: () => 0, // dummy
|
||||
proc_exit: (code) => {
|
||||
this.exited = true;
|
||||
this.exitCode = code;
|
||||
this._resolveExitPromise();
|
||||
throw wasmExit;
|
||||
},
|
||||
random_get: (bufPtr, bufLen) => {
|
||||
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
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 ticks() int64
|
||||
"runtime.ticks": () => {
|
||||
return BigInt((timeOrigin + performance.now()) * 1e6);
|
||||
},
|
||||
|
||||
// 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 sleepTicks(timeout int64)
|
||||
"runtime.sleepTicks": (timeout) => {
|
||||
// Do not sleep, only reactivate scheduler after the given timeout.
|
||||
setTimeout(() => {
|
||||
if (this.exited) return;
|
||||
try {
|
||||
this._inst.exports.go_scheduler();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
}, Number(timeout)/1e6);
|
||||
},
|
||||
|
||||
// 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);
|
||||
"syscall/js.finalizeRef": (v_ref) => {
|
||||
// Note: TinyGo does not support finalizers so this is only called
|
||||
// for one specific case, by js.go:jsString. and can/might leak memory.
|
||||
const id = v_ref & 0xffffffffn;
|
||||
if (this._goRefCounts?.[id] !== undefined) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
console.error("syscall/js.finalizeRef: unknown id", id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
"syscall/js.stringVal": (value_ptr, value_len) => {
|
||||
value_ptr >>>= 0;
|
||||
const s = loadString(value_ptr, value_len);
|
||||
return boxValue(s);
|
||||
},
|
||||
|
||||
// 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);
|
||||
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
|
||||
let prop = loadString(p_ptr, p_len);
|
||||
let v = unboxValue(v_ref);
|
||||
let result = Reflect.get(v, prop);
|
||||
return boxValue(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));
|
||||
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
const x = unboxValue(x_ref);
|
||||
Reflect.set(v, p, x);
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
Reflect.deleteProperty(v, p);
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
"syscall/js.valueIndex": (v_ref, i) => {
|
||||
return boxValue(Reflect.get(unboxValue(v_ref), i));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
|
||||
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const name = loadString(m_ptr, m_len);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
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);
|
||||
const m = Reflect.get(v, name);
|
||||
storeValue(ret_addr, Reflect.apply(m, v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
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);
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
storeValue(ret_addr, Reflect.apply(v, undefined, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
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);
|
||||
storeValue(ret_addr, Reflect.construct(v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr+ 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
"syscall/js.valueLength": (v_ref) => {
|
||||
return unboxValue(v_ref).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);
|
||||
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
|
||||
const s = String(unboxValue(v_ref));
|
||||
const str = encoder.encode(s);
|
||||
storeValue(ret_addr, str);
|
||||
mem().setInt32(ret_addr + 8, str.length, true);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
|
||||
const str = unboxValue(v_ref);
|
||||
loadSlice(slice_ptr, slice_len, slice_cap).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);
|
||||
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
|
||||
return unboxValue(v_ref) instanceof unboxValue(t_ref);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = loadSlice(dest_addr, dest_len);
|
||||
const src = unboxValue(src_ref);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
// copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
// Originally copied from upstream Go project, then modified:
|
||||
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
|
||||
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = unboxValue(dst_ref);
|
||||
const src = loadSlice(src_addr, src_len);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
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);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
|
||||
// For compatibility, we use both as long as Go 1.20 is supported.
|
||||
this.importObject.env = this.importObject.gojs;
|
||||
}
|
||||
|
||||
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,
|
||||
global,
|
||||
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
|
||||
this._goRefCounts = []; // 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
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
this.exitCode = 0;
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
if (this._inst.exports._start) {
|
||||
let exitPromise = new Promise((resolve, reject) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
|
||||
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);
|
||||
// Run program, but catch the wasmExit exception that's thrown
|
||||
// to return back here.
|
||||
try {
|
||||
this._inst.exports._start();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
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");
|
||||
await exitPromise;
|
||||
return this.exitCode;
|
||||
} else {
|
||||
this._inst.exports._initialize();
|
||||
}
|
||||
|
||||
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();
|
||||
try {
|
||||
this._inst.exports.resume();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
@@ -558,4 +528,26 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
global.require &&
|
||||
global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length != 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
|
||||
let exitCode = await go.run(result.instance);
|
||||
process.exit(exitCode);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,566 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -1,722 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,821 +0,0 @@
|
||||
/**
|
||||
* 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)) {
|
||||
// 继续跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ import * as shellPrettierPlugin from "@/common/prettier/plugins/shell";
|
||||
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 clangPrettierPlugin from "@/common/prettier/plugins/clang";
|
||||
import * as prettierPluginEstree from "prettier/plugins/estree";
|
||||
|
||||
/**
|
||||
@@ -103,7 +103,10 @@ export const LANGUAGES: LanguageInfo[] = [
|
||||
parser: "xml",
|
||||
plugins: [xmlPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("cpp", "C++", cppLanguage.parser),
|
||||
new LanguageInfo("cpp", "C++", cppLanguage.parser,{
|
||||
parser: "clang",
|
||||
plugins: [clangPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("rs", "Rust", rustLanguage.parser,{
|
||||
parser: "jinx-rust",
|
||||
plugins: [rustPrettierPlugin]
|
||||
@@ -146,10 +149,7 @@ export const LANGUAGES: LanguageInfo[] = [
|
||||
parser: "groovy",
|
||||
plugins: [groovyPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser,{
|
||||
parser: "powershell",
|
||||
plugins: [powershellPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
|
||||
new LanguageInfo("dart", "Dart", null), // 暂无解析器
|
||||
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser,{
|
||||
parser: "scala",
|
||||
|
||||
Reference in New Issue
Block a user