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
|
@echo off
|
||||||
rem Build script for Go Prettier Plugin WASM
|
rem Build script for Go Prettier Plugin WASM using native Go
|
||||||
rem This script compiles the Go code to WebAssembly
|
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 GOOS=js
|
||||||
set GOARCH=wasm
|
set GOARCH=wasm
|
||||||
|
|
||||||
rem Build the WASM file
|
rem Build the WASM file using native Go
|
||||||
echo Compiling main.go to go.wasm...
|
echo Compiling main.go to go.wasm with Go...
|
||||||
go build -o go.wasm main.go
|
go build -o go-format.wasm main.go
|
||||||
|
|
||||||
if %ERRORLEVEL% EQU 0 (
|
if %ERRORLEVEL% EQU 0 (
|
||||||
echo ✅ Build successful!
|
echo Build successful!
|
||||||
|
|
||||||
rem Show file size (Windows version)
|
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
|
rem Copy to public directory for browser access
|
||||||
if exist "..\..\..\..\..\public" (
|
if exist "..\..\..\..\..\public" (
|
||||||
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
|
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 (
|
) else (
|
||||||
echo ❌ Build failed!
|
echo Build failed!
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
@@ -1,30 +1,42 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Build script for Go Prettier Plugin WASM
|
# Build script for Go Prettier Plugin WASM using native Go
|
||||||
# This script compiles the Go code to WebAssembly
|
# 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 GOOS=js
|
||||||
export GOARCH=wasm
|
export GOARCH=wasm
|
||||||
|
|
||||||
# Build the WASM file
|
# Build the WASM file using native Go
|
||||||
echo "Compiling main.go to go.wasm..."
|
echo "Compiling main.go to go.wasm with Go..."
|
||||||
go build -o go.wasm main.go
|
go build -o go-format.wasm main.go
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✅ Build successful!"
|
echo "Build successful!"
|
||||||
echo "📊 WASM file size: $(du -h go.wasm | cut -f1)"
|
echo "WASM file size: $(du -h go-format.wasm | cut -f1)"
|
||||||
|
|
||||||
# Copy to public directory for browser access
|
# Copy to public directory for browser access
|
||||||
if [ -d "../../../../../public" ]; then
|
if [ -d "../../../../../public" ]; then
|
||||||
cp go.wasm ../../../../../public/go.wasm
|
cp go-format.wasm ../../../../../public/go-format.wasm
|
||||||
echo "📋 Copied to public directory"
|
echo "Copied to public directory"
|
||||||
|
rm go-format.wasm
|
||||||
|
echo "Cleaned up local WASM file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🎉 Go Prettier Plugin WASM is ready!"
|
echo "Go Prettier Plugin WASM is ready!"
|
||||||
else
|
else
|
||||||
echo "❌ Build failed!"
|
echo "Build failed!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1,244 +1,142 @@
|
|||||||
/**
|
/**
|
||||||
* Go Prettier Plugin - Universal Implementation
|
* @fileoverview Go Prettier Format Plugin
|
||||||
* WebAssembly-based Go code formatter for Prettier
|
* A Prettier plugin for formatting Go code using WebAssembly.
|
||||||
* Supports both Node.js and Browser environments
|
* This plugin leverages Go's native formatting capabilities through WASM.
|
||||||
*/
|
*/
|
||||||
|
import "./wasm_exec.js"
|
||||||
|
/** @type {Promise<void>|null} */
|
||||||
|
let initializePromise;
|
||||||
|
|
||||||
let initializePromise = null;
|
/**
|
||||||
|
* Initializes the Go WebAssembly module for formatting Go code.
|
||||||
// Environment detection
|
* This function sets up the WASM runtime and makes the formatGo function
|
||||||
const isNode = () => {
|
* available on the global object.
|
||||||
return typeof process !== 'undefined' &&
|
*
|
||||||
process.versions != null &&
|
* @async
|
||||||
process.versions.node != null;
|
* @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
|
||||||
const isBrowser = () => {
|
*/
|
||||||
return typeof window !== 'undefined' &&
|
function initialize() {
|
||||||
typeof document !== 'undefined';
|
if (initializePromise) {
|
||||||
};
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
})();
|
|
||||||
|
|
||||||
return initializePromise;
|
return initializePromise;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const languages = [
|
initializePromise = (async () => {
|
||||||
{
|
|
||||||
name: "Go",
|
const go = new TinyGo();
|
||||||
parsers: ["go-format"],
|
|
||||||
extensions: [".go"],
|
// Load WASM file from browser
|
||||||
vscodeLanguageIds: ["go"],
|
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": {
|
* Prettier parser configuration for Go.
|
||||||
parse: (text) => text,
|
* Defines how Go source code should be parsed and processed.
|
||||||
astFormat: "go-format",
|
*
|
||||||
locStart: (node) => 0,
|
* @type {Object<string, Object>}
|
||||||
locEnd: (node) => node.length,
|
* @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 = {
|
export default { languages, parsers, printers, initialize };
|
||||||
"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
|
|
||||||
};
|
|
||||||
Binary file not shown.
@@ -4,8 +4,8 @@
|
|||||||
// functionality for the Prettier plugin. This package exposes the formatGo function
|
// 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.
|
// 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
|
// The module is designed to be compiled to WebAssembly using native Go (GOOS=js GOARCH=wasm)
|
||||||
// environments as part of the go-prettier-format plugin.
|
// and loaded in browser environments as part of the Go Prettier plugin.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,26 +1,53 @@
|
|||||||
// Copyright 2018 The Go Authors. All rights reserved.
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// 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 enosys = () => {
|
||||||
const err = new Error("not implemented");
|
const err = new Error("not implemented");
|
||||||
err.code = "ENOSYS";
|
err.code = "ENOSYS";
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!globalThis.fs) {
|
if (!global.fs) {
|
||||||
let outputBuf = "";
|
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
|
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||||
writeSync(fd, buf) {
|
writeSync(fd, buf) {
|
||||||
outputBuf += decoder.decode(buf);
|
outputBuf += decoder.decode(buf);
|
||||||
const nl = outputBuf.lastIndexOf("\n");
|
const nl = outputBuf.lastIndexOf("\n");
|
||||||
if (nl != -1) {
|
if (nl != -1) {
|
||||||
console.log(outputBuf.substring(0, nl));
|
console.log(outputBuf.substr(0, nl));
|
||||||
outputBuf = outputBuf.substring(nl + 1);
|
outputBuf = outputBuf.substr(nl + 1);
|
||||||
}
|
}
|
||||||
return buf.length;
|
return buf.length;
|
||||||
},
|
},
|
||||||
@@ -58,8 +85,8 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globalThis.process) {
|
if (!global.process) {
|
||||||
globalThis.process = {
|
global.process = {
|
||||||
getuid() { return -1; },
|
getuid() { return -1; },
|
||||||
getgid() { return -1; },
|
getgid() { return -1; },
|
||||||
geteuid() { return -1; },
|
geteuid() { return -1; },
|
||||||
@@ -73,58 +100,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globalThis.crypto) {
|
if (!global.crypto) {
|
||||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
const nodeCrypto = require("node:crypto");
|
||||||
|
global.crypto = {
|
||||||
|
getRandomValues(b) {
|
||||||
|
nodeCrypto.randomFillSync(b);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globalThis.performance) {
|
if (!global.performance) {
|
||||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
global.performance = {
|
||||||
|
now() {
|
||||||
|
const [sec, nsec] = process.hrtime();
|
||||||
|
return sec * 1000 + nsec / 1000000;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globalThis.TextEncoder) {
|
if (!global.TextEncoder) {
|
||||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
global.TextEncoder = require("node:util").TextEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globalThis.TextDecoder) {
|
if (!global.TextDecoder) {
|
||||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
global.TextDecoder = require("node:util").TextDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End of polyfills for common API.
|
||||||
|
|
||||||
const encoder = new TextEncoder("utf-8");
|
const encoder = new TextEncoder("utf-8");
|
||||||
const decoder = new TextDecoder("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() {
|
constructor() {
|
||||||
this.argv = ["js"];
|
this._callbackTimeouts = new Map();
|
||||||
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;
|
this._nextCallbackTimeoutID = 1;
|
||||||
|
|
||||||
const setInt64 = (addr, v) => {
|
const mem = () => {
|
||||||
this.mem.setUint32(addr + 0, v, true);
|
// The buffer may change when requesting more memory.
|
||||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
return new DataView(this._inst.exports.memory.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setInt32 = (addr, v) => {
|
const unboxValue = (v_ref) => {
|
||||||
this.mem.setUint32(addr + 0, v, true);
|
reinterpretBuf.setBigInt64(0, v_ref, true);
|
||||||
}
|
const f = reinterpretBuf.getFloat64(0, 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) {
|
if (f === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -132,69 +154,77 @@
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = this.mem.getUint32(addr, true);
|
const id = v_ref & 0xffffffffn;
|
||||||
return this._values[id];
|
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)) {
|
if (isNaN(v)) {
|
||||||
this.mem.setUint32(addr + 4, nanHead, true);
|
return nanHead << 32n;
|
||||||
this.mem.setUint32(addr, 0, true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.mem.setFloat64(addr, v, true);
|
if (v === 0) {
|
||||||
return;
|
return (nanHead << 32n) | 1n;
|
||||||
|
}
|
||||||
|
reinterpretBuf.setFloat64(0, v, true);
|
||||||
|
return reinterpretBuf.getBigInt64(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v === undefined) {
|
switch (v) {
|
||||||
this.mem.setFloat64(addr, 0, true);
|
case undefined:
|
||||||
return;
|
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);
|
let id = this._ids.get(v);
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
id = this._idPool.pop();
|
id = this._idPool.pop();
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
id = this._values.length;
|
id = BigInt(this._values.length);
|
||||||
}
|
}
|
||||||
this._values[id] = v;
|
this._values[id] = v;
|
||||||
this._goRefCounts[id] = 0;
|
this._goRefCounts[id] = 0;
|
||||||
this._ids.set(v, id);
|
this._ids.set(v, id);
|
||||||
}
|
}
|
||||||
this._goRefCounts[id]++;
|
this._goRefCounts[id]++;
|
||||||
let typeFlag = 0;
|
let typeFlag = 1n;
|
||||||
switch (typeof v) {
|
switch (typeof v) {
|
||||||
case "object":
|
|
||||||
if (v !== null) {
|
|
||||||
typeFlag = 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "string":
|
case "string":
|
||||||
typeFlag = 2;
|
typeFlag = 2n;
|
||||||
break;
|
break;
|
||||||
case "symbol":
|
case "symbol":
|
||||||
typeFlag = 3;
|
typeFlag = 3n;
|
||||||
break;
|
break;
|
||||||
case "function":
|
case "function":
|
||||||
typeFlag = 4;
|
typeFlag = 4n;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
return id | ((nanHead | typeFlag) << 32n);
|
||||||
this.mem.setUint32(addr, id, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSlice = (addr) => {
|
const storeValue = (addr, v) => {
|
||||||
const array = getInt64(addr + 0);
|
let v_ref = boxValue(v);
|
||||||
const len = getInt64(addr + 8);
|
mem().setBigUint64(addr, v_ref, true);
|
||||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSliceOfValues = (addr) => {
|
const loadSlice = (array, len, cap) => {
|
||||||
const array = getInt64(addr + 0);
|
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
|
||||||
const len = getInt64(addr + 8);
|
}
|
||||||
|
|
||||||
|
const loadSliceOfValues = (array, len, cap) => {
|
||||||
const a = new Array(len);
|
const a = new Array(len);
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
a[i] = loadValue(array + i * 8);
|
a[i] = loadValue(array + i * 8);
|
||||||
@@ -202,347 +232,287 @@
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadString = (addr) => {
|
const loadString = (ptr, len) => {
|
||||||
const saddr = getInt64(addr + 0);
|
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
|
||||||
const len = getInt64(addr + 8);
|
|
||||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeOrigin = Date.now() - performance.now();
|
const timeOrigin = Date.now() - performance.now();
|
||||||
this.importObject = {
|
this.importObject = {
|
||||||
_gotest: {
|
wasi_snapshot_preview1: {
|
||||||
add: (a, b) => a + b,
|
// 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: {
|
gojs: {
|
||||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
// func ticks() int64
|
||||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
"runtime.ticks": () => {
|
||||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
return BigInt((timeOrigin + performance.now()) * 1e6);
|
||||||
// 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)
|
// func sleepTicks(timeout int64)
|
||||||
"runtime.wasmWrite": (sp) => {
|
"runtime.sleepTicks": (timeout) => {
|
||||||
sp >>>= 0;
|
// Do not sleep, only reactivate scheduler after the given timeout.
|
||||||
const fd = getInt64(sp + 8);
|
setTimeout(() => {
|
||||||
const p = getInt64(sp + 16);
|
if (this.exited) return;
|
||||||
const n = this.mem.getInt32(sp + 24, true);
|
try {
|
||||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
this._inst.exports.go_scheduler();
|
||||||
},
|
} catch (e) {
|
||||||
|
if (e !== wasmExit) throw e;
|
||||||
// func resetMemoryDataView()
|
}
|
||||||
"runtime.resetMemoryDataView": (sp) => {
|
}, Number(timeout)/1e6);
|
||||||
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)
|
// func finalizeRef(v ref)
|
||||||
"syscall/js.finalizeRef": (sp) => {
|
"syscall/js.finalizeRef": (v_ref) => {
|
||||||
sp >>>= 0;
|
// Note: TinyGo does not support finalizers so this is only called
|
||||||
const id = this.mem.getUint32(sp + 8, true);
|
// for one specific case, by js.go:jsString. and can/might leak memory.
|
||||||
this._goRefCounts[id]--;
|
const id = v_ref & 0xffffffffn;
|
||||||
if (this._goRefCounts[id] === 0) {
|
if (this._goRefCounts?.[id] !== undefined) {
|
||||||
const v = this._values[id];
|
this._goRefCounts[id]--;
|
||||||
this._values[id] = null;
|
if (this._goRefCounts[id] === 0) {
|
||||||
this._ids.delete(v);
|
const v = this._values[id];
|
||||||
this._idPool.push(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
|
// func stringVal(value string) ref
|
||||||
"syscall/js.stringVal": (sp) => {
|
"syscall/js.stringVal": (value_ptr, value_len) => {
|
||||||
sp >>>= 0;
|
value_ptr >>>= 0;
|
||||||
storeValue(sp + 24, loadString(sp + 8));
|
const s = loadString(value_ptr, value_len);
|
||||||
|
return boxValue(s);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueGet(v ref, p string) ref
|
// func valueGet(v ref, p string) ref
|
||||||
"syscall/js.valueGet": (sp) => {
|
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
|
||||||
sp >>>= 0;
|
let prop = loadString(p_ptr, p_len);
|
||||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
let v = unboxValue(v_ref);
|
||||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
let result = Reflect.get(v, prop);
|
||||||
storeValue(sp + 32, result);
|
return boxValue(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueSet(v ref, p string, x ref)
|
// func valueSet(v ref, p string, x ref)
|
||||||
"syscall/js.valueSet": (sp) => {
|
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
|
||||||
sp >>>= 0;
|
const v = unboxValue(v_ref);
|
||||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
const p = loadString(p_ptr, p_len);
|
||||||
|
const x = unboxValue(x_ref);
|
||||||
|
Reflect.set(v, p, x);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueDelete(v ref, p string)
|
// func valueDelete(v ref, p string)
|
||||||
"syscall/js.valueDelete": (sp) => {
|
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
|
||||||
sp >>>= 0;
|
const v = unboxValue(v_ref);
|
||||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
const p = loadString(p_ptr, p_len);
|
||||||
|
Reflect.deleteProperty(v, p);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueIndex(v ref, i int) ref
|
// func valueIndex(v ref, i int) ref
|
||||||
"syscall/js.valueIndex": (sp) => {
|
"syscall/js.valueIndex": (v_ref, i) => {
|
||||||
sp >>>= 0;
|
return boxValue(Reflect.get(unboxValue(v_ref), i));
|
||||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// valueSetIndex(v ref, i int, x ref)
|
// valueSetIndex(v ref, i int, x ref)
|
||||||
"syscall/js.valueSetIndex": (sp) => {
|
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
|
||||||
sp >>>= 0;
|
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
|
||||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||||
"syscall/js.valueCall": (sp) => {
|
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
|
||||||
sp >>>= 0;
|
const v = unboxValue(v_ref);
|
||||||
|
const name = loadString(m_ptr, m_len);
|
||||||
|
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||||
try {
|
try {
|
||||||
const v = loadValue(sp + 8);
|
const m = Reflect.get(v, name);
|
||||||
const m = Reflect.get(v, loadString(sp + 16));
|
storeValue(ret_addr, Reflect.apply(m, v, args));
|
||||||
const args = loadSliceOfValues(sp + 32);
|
mem().setUint8(ret_addr + 8, 1);
|
||||||
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) {
|
} catch (err) {
|
||||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
storeValue(ret_addr, err);
|
||||||
storeValue(sp + 56, err);
|
mem().setUint8(ret_addr + 8, 0);
|
||||||
this.mem.setUint8(sp + 64, 0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||||
"syscall/js.valueInvoke": (sp) => {
|
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||||
sp >>>= 0;
|
|
||||||
try {
|
try {
|
||||||
const v = loadValue(sp + 8);
|
const v = unboxValue(v_ref);
|
||||||
const args = loadSliceOfValues(sp + 16);
|
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||||
const result = Reflect.apply(v, undefined, args);
|
storeValue(ret_addr, Reflect.apply(v, undefined, args));
|
||||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
mem().setUint8(ret_addr + 8, 1);
|
||||||
storeValue(sp + 40, result);
|
|
||||||
this.mem.setUint8(sp + 48, 1);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
storeValue(ret_addr, err);
|
||||||
storeValue(sp + 40, err);
|
mem().setUint8(ret_addr + 8, 0);
|
||||||
this.mem.setUint8(sp + 48, 0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueNew(v ref, args []ref) (ref, bool)
|
// func valueNew(v ref, args []ref) (ref, bool)
|
||||||
"syscall/js.valueNew": (sp) => {
|
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||||
sp >>>= 0;
|
const v = unboxValue(v_ref);
|
||||||
|
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||||
try {
|
try {
|
||||||
const v = loadValue(sp + 8);
|
storeValue(ret_addr, Reflect.construct(v, args));
|
||||||
const args = loadSliceOfValues(sp + 16);
|
mem().setUint8(ret_addr + 8, 1);
|
||||||
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) {
|
} catch (err) {
|
||||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
storeValue(ret_addr, err);
|
||||||
storeValue(sp + 40, err);
|
mem().setUint8(ret_addr+ 8, 0);
|
||||||
this.mem.setUint8(sp + 48, 0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueLength(v ref) int
|
// func valueLength(v ref) int
|
||||||
"syscall/js.valueLength": (sp) => {
|
"syscall/js.valueLength": (v_ref) => {
|
||||||
sp >>>= 0;
|
return unboxValue(v_ref).length;
|
||||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// valuePrepareString(v ref) (ref, int)
|
// valuePrepareString(v ref) (ref, int)
|
||||||
"syscall/js.valuePrepareString": (sp) => {
|
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
|
||||||
sp >>>= 0;
|
const s = String(unboxValue(v_ref));
|
||||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
const str = encoder.encode(s);
|
||||||
storeValue(sp + 16, str);
|
storeValue(ret_addr, str);
|
||||||
setInt64(sp + 24, str.length);
|
mem().setInt32(ret_addr + 8, str.length, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
// valueLoadString(v ref, b []byte)
|
// valueLoadString(v ref, b []byte)
|
||||||
"syscall/js.valueLoadString": (sp) => {
|
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
|
||||||
sp >>>= 0;
|
const str = unboxValue(v_ref);
|
||||||
const str = loadValue(sp + 8);
|
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
|
||||||
loadSlice(sp + 16).set(str);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// func valueInstanceOf(v ref, t ref) bool
|
// func valueInstanceOf(v ref, t ref) bool
|
||||||
"syscall/js.valueInstanceOf": (sp) => {
|
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
|
||||||
sp >>>= 0;
|
return unboxValue(v_ref) instanceof unboxValue(t_ref);
|
||||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||||
"syscall/js.copyBytesToGo": (sp) => {
|
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
|
||||||
sp >>>= 0;
|
let num_bytes_copied_addr = ret_addr;
|
||||||
const dst = loadSlice(sp + 8);
|
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||||
const src = loadValue(sp + 32);
|
|
||||||
|
const dst = loadSlice(dest_addr, dest_len);
|
||||||
|
const src = unboxValue(src_ref);
|
||||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||||
this.mem.setUint8(sp + 48, 0);
|
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toCopy = src.subarray(0, dst.length);
|
const toCopy = src.subarray(0, dst.length);
|
||||||
dst.set(toCopy);
|
dst.set(toCopy);
|
||||||
setInt64(sp + 40, toCopy.length);
|
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||||
this.mem.setUint8(sp + 48, 1);
|
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||||
},
|
},
|
||||||
|
|
||||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
// copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||||
"syscall/js.copyBytesToJS": (sp) => {
|
// Originally copied from upstream Go project, then modified:
|
||||||
sp >>>= 0;
|
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
|
||||||
const dst = loadValue(sp + 8);
|
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
|
||||||
const src = loadSlice(sp + 16);
|
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)) {
|
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||||
this.mem.setUint8(sp + 48, 0);
|
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toCopy = src.subarray(0, dst.length);
|
const toCopy = src.subarray(0, dst.length);
|
||||||
dst.set(toCopy);
|
dst.set(toCopy);
|
||||||
setInt64(sp + 40, toCopy.length);
|
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||||
this.mem.setUint8(sp + 48, 1);
|
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||||
},
|
|
||||||
|
|
||||||
"debug": (value) => {
|
|
||||||
console.log(value);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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) {
|
async run(instance) {
|
||||||
if (!(instance instanceof WebAssembly.Instance)) {
|
|
||||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
|
||||||
}
|
|
||||||
this._inst = instance;
|
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
|
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||||
NaN,
|
NaN,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
globalThis,
|
global,
|
||||||
this,
|
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._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._ids = new Map(); // mapping from JS values to reference ids
|
||||||
[0, 1],
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
[null, 2],
|
this.exited = false; // whether the Go program has exited
|
||||||
[true, 3],
|
this.exitCode = 0;
|
||||||
[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.
|
if (this._inst.exports._start) {
|
||||||
let offset = 4096;
|
let exitPromise = new Promise((resolve, reject) => {
|
||||||
|
this._resolveExitPromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
const strPtr = (str) => {
|
// Run program, but catch the wasmExit exception that's thrown
|
||||||
const ptr = offset;
|
// to return back here.
|
||||||
const bytes = encoder.encode(str + "\0");
|
try {
|
||||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
this._inst.exports._start();
|
||||||
offset += bytes.length;
|
} catch (e) {
|
||||||
if (offset % 8 !== 0) {
|
if (e !== wasmExit) throw e;
|
||||||
offset += 8 - (offset % 8);
|
|
||||||
}
|
}
|
||||||
return ptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
const argc = this.argv.length;
|
await exitPromise;
|
||||||
|
return this.exitCode;
|
||||||
const argvPtrs = [];
|
} else {
|
||||||
this.argv.forEach((arg) => {
|
this._inst.exports._initialize();
|
||||||
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() {
|
_resume() {
|
||||||
if (this.exited) {
|
if (this.exited) {
|
||||||
throw new Error("Go program has already 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) {
|
if (this.exited) {
|
||||||
this._resolveExitPromise();
|
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 tomlPrettierPlugin from "@/common/prettier/plugins/toml";
|
||||||
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
|
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
|
||||||
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
|
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
|
||||||
import powershellPrettierPlugin from "@/common/prettier/plugins/powershell";
|
|
||||||
import scalaPrettierPlugin from "@/common/prettier/plugins/scala";
|
import scalaPrettierPlugin from "@/common/prettier/plugins/scala";
|
||||||
|
import clangPrettierPlugin from "@/common/prettier/plugins/clang";
|
||||||
import * as prettierPluginEstree from "prettier/plugins/estree";
|
import * as prettierPluginEstree from "prettier/plugins/estree";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,7 +103,10 @@ export const LANGUAGES: LanguageInfo[] = [
|
|||||||
parser: "xml",
|
parser: "xml",
|
||||||
plugins: [xmlPrettierPlugin]
|
plugins: [xmlPrettierPlugin]
|
||||||
}),
|
}),
|
||||||
new LanguageInfo("cpp", "C++", cppLanguage.parser),
|
new LanguageInfo("cpp", "C++", cppLanguage.parser,{
|
||||||
|
parser: "clang",
|
||||||
|
plugins: [clangPrettierPlugin]
|
||||||
|
}),
|
||||||
new LanguageInfo("rs", "Rust", rustLanguage.parser,{
|
new LanguageInfo("rs", "Rust", rustLanguage.parser,{
|
||||||
parser: "jinx-rust",
|
parser: "jinx-rust",
|
||||||
plugins: [rustPrettierPlugin]
|
plugins: [rustPrettierPlugin]
|
||||||
@@ -146,10 +149,7 @@ export const LANGUAGES: LanguageInfo[] = [
|
|||||||
parser: "groovy",
|
parser: "groovy",
|
||||||
plugins: [groovyPrettierPlugin]
|
plugins: [groovyPrettierPlugin]
|
||||||
}),
|
}),
|
||||||
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser,{
|
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
|
||||||
parser: "powershell",
|
|
||||||
plugins: [powershellPrettierPlugin]
|
|
||||||
}),
|
|
||||||
new LanguageInfo("dart", "Dart", null), // 暂无解析器
|
new LanguageInfo("dart", "Dart", null), // 暂无解析器
|
||||||
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser,{
|
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser,{
|
||||||
parser: "scala",
|
parser: "scala",
|
||||||
|
|||||||
Reference in New Issue
Block a user