3 Commits

Author SHA1 Message Date
f72010bd69 Added clang prettier plugin 2025-09-19 19:17:13 +08:00
cd027097f8 🐛 Fixed golang prettier plugin issue 2025-09-19 18:39:41 +08:00
9cbbf729c0 🔥 Remove powershell prettier plugin 2025-09-18 00:13:07 +08:00
37 changed files with 3552 additions and 4294 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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;
};
}
}
})();

View 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 &current_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

View File

@@ -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

View 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

View 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())

View File

@@ -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";

View File

@@ -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";

View 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 Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts 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 Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts 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 Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts 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 Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts 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;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
Module.preRun = function customPreRun() {
ENV.PWD = process.cwd();
}

View 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() << "&#10;";
break;
case '\r':
outs() << "&#13;";
break;
case '<':
outs() << "&lt;";
break;
case '&':
outs() << "&amp;";
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;
}

View 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())

View 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;

View 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);
}

View 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

View 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,
};

View 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!

View 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!"

View File

@@ -1,32 +1,43 @@
@echo off
rem Build script for Go Prettier Plugin WASM
rem This script compiles the Go code to WebAssembly
rem Build script for Go Prettier Plugin WASM using native Go
rem This script compiles the Go code to WebAssembly for browser environment
echo 🔨 Building Go Prettier Plugin WASM...
echo Building Go Prettier Plugin WASM with native Go...
rem Set WASM build environment
rem Check if Go is available
go version >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo Go not found! Please install Go 1.21+ first.
echo Visit: https://golang.org/dl/
pause
exit /b 1
)
rem Set WASM build environment for browser (js/wasm)
set GOOS=js
set GOARCH=wasm
rem Build the WASM file
echo Compiling main.go to go.wasm...
go build -o go.wasm main.go
rem Build the WASM file using native Go
echo Compiling main.go to go.wasm with Go...
go build -o go-format.wasm main.go
if %ERRORLEVEL% EQU 0 (
echo Build successful!
echo Build successful!
rem Show file size (Windows version)
for %%A in (go.wasm) do echo 📊 WASM file size: %%~zA bytes
for %%A in (go.wasm) do echo WASM file size: %%~zA bytes
rem Copy to public directory for browser access
if exist "..\..\..\..\..\public" (
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
echo 📋 Copied to public directory
echo Copied to public directory
del go.wasm
echo Cleaned up local WASM file
)
echo 🎉 Go Prettier Plugin WASM is ready!
echo Go Prettier Plugin WASM is ready!
) else (
echo Build failed!
echo Build failed!
pause
exit /b 1
)
)

View File

@@ -1,30 +1,42 @@
#!/bin/bash
# Build script for Go Prettier Plugin WASM
# This script compiles the Go code to WebAssembly
# Build script for Go Prettier Plugin WASM using native Go
# This script compiles the Go code to WebAssembly for browser environment
echo "🔨 Building Go Prettier Plugin WASM..."
echo "Building Go Prettier Plugin WASM with native Go..."
# Set WASM build environment
# Check if Go is available
if ! command -v go &> /dev/null; then
echo "Go not found! Please install Go 1.21+ first."
echo "Visit: https://golang.org/dl/"
exit 1
fi
# Display Go version
echo "Using Go version: $(go version)"
# Set WASM build environment for browser (js/wasm)
export GOOS=js
export GOARCH=wasm
# Build the WASM file
echo "Compiling main.go to go.wasm..."
go build -o go.wasm main.go
# Build the WASM file using native Go
echo "Compiling main.go to go.wasm with Go..."
go build -o go-format.wasm main.go
if [ $? -eq 0 ]; then
echo "Build successful!"
echo "📊 WASM file size: $(du -h go.wasm | cut -f1)"
echo "Build successful!"
echo "WASM file size: $(du -h go-format.wasm | cut -f1)"
# Copy to public directory for browser access
if [ -d "../../../../../public" ]; then
cp go.wasm ../../../../../public/go.wasm
echo "📋 Copied to public directory"
cp go-format.wasm ../../../../../public/go-format.wasm
echo "Copied to public directory"
rm go-format.wasm
echo "Cleaned up local WASM file"
fi
echo "🎉 Go Prettier Plugin WASM is ready!"
echo "Go Prettier Plugin WASM is ready!"
else
echo "Build failed!"
echo "Build failed!"
exit 1
fi
fi

View File

@@ -1,244 +1,142 @@
/**
* Go Prettier Plugin - Universal Implementation
* WebAssembly-based Go code formatter for Prettier
* Supports both Node.js and Browser environments
* @fileoverview Go Prettier Format Plugin
* A Prettier plugin for formatting Go code using WebAssembly.
* This plugin leverages Go's native formatting capabilities through WASM.
*/
import "./wasm_exec.js"
/** @type {Promise<void>|null} */
let initializePromise;
let initializePromise = null;
// Environment detection
const isNode = () => {
return typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null;
};
const isBrowser = () => {
return typeof window !== 'undefined' &&
typeof document !== 'undefined';
};
// Node.js WASM loading
const loadWasmNode = async () => {
try {
const fs = await import('fs');
const path = await import('path');
const { fileURLToPath } = await import('url');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const wasmPath = path.join(__dirname, 'go.wasm');
return fs.readFileSync(wasmPath);
} catch (error) {
console.error('Node.js WASM loading failed:', error);
throw error;
}
};
// Browser WASM loading
const loadWasmBrowser = async () => {
try {
const response = await fetch('/go.wasm');
if (!response.ok) {
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
}
return await response.arrayBuffer();
} catch (error) {
console.error('Browser WASM loading failed:', error);
throw error;
}
};
// Node.js Go runtime initialization
const initGoRuntimeNode = async () => {
if (globalThis.Go) return;
try {
// Dynamic import of wasm_exec.js for Node.js
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
const path = await import('path');
const { fileURLToPath } = await import('url');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load wasm_exec.js
const wasmExecPath = path.join(__dirname, 'wasm_exec.js');
require(wasmExecPath);
if (!globalThis.Go) {
throw new Error('Go WASM runtime not available after loading wasm_exec.js');
}
} catch (error) {
console.error('Node.js Go runtime initialization failed:', error);
throw error;
}
};
// Browser Go runtime initialization
const initGoRuntimeBrowser = async () => {
// 总是重新初始化,因为可能存在版本不兼容问题
try {
// 移除旧的 Go 运行时
delete globalThis.Go;
// 动态导入本地的 wasm_exec.js 内容
const wasmExecResponse = await fetch('/wasm_exec.js');
if (!wasmExecResponse.ok) {
throw new Error(`Failed to fetch wasm_exec.js: ${wasmExecResponse.status}`);
}
const wasmExecCode = await wasmExecResponse.text();
// 在全局作用域中执行 wasm_exec.js 代码
const script = document.createElement('script');
script.textContent = wasmExecCode;
document.head.appendChild(script);
// 等待一小段时间确保脚本执行完成
await new Promise(resolve => setTimeout(resolve, 100));
if (!globalThis.Go) {
throw new Error('Go WASM runtime not available after executing wasm_exec.js');
}
console.log('Go runtime initialized successfully');
} catch (error) {
console.error('Browser Go runtime initialization failed:', error);
throw error;
}
};
// Universal initialization
const initialize = async () => {
if (initializePromise) return initializePromise;
initializePromise = (async () => {
let wasmBuffer;
console.log('Starting Go WASM initialization...');
// Environment-specific initialization
if (isNode()) {
console.log('Initializing for Node.js environment');
await initGoRuntimeNode();
wasmBuffer = await loadWasmNode();
} else if (isBrowser()) {
console.log('Initializing for Browser environment');
await initGoRuntimeBrowser();
wasmBuffer = await loadWasmBrowser();
} else {
throw new Error('Unsupported environment: neither Node.js nor Browser detected');
}
console.log('Creating Go instance...');
const go = new globalThis.Go();
// 详细检查 importObject
console.log('Go import object keys:', Object.keys(go.importObject));
if (go.importObject.gojs) {
console.log('gojs import keys:', Object.keys(go.importObject.gojs));
console.log('scheduleTimeoutEvent type:', typeof go.importObject.gojs['runtime.scheduleTimeoutEvent']);
}
console.log('Instantiating WebAssembly module...');
try {
const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject);
console.log('WebAssembly instantiation successful');
console.log('Running Go program...');
// Run Go program (don't await as it's a long-running service)
go.run(instance).catch(err => {
console.error('Go WASM program exit error:', err);
});
} catch (instantiateError) {
console.error('WebAssembly instantiation failed:', instantiateError);
console.error('Error details:', {
message: instantiateError.message,
name: instantiateError.name,
stack: instantiateError.stack
});
throw instantiateError;
}
// Wait for Go program to initialize and expose formatGo function
console.log('Waiting for formatGo function to be available...');
let retries = 0;
const maxRetries = 20; // 增加重试次数
while (typeof globalThis.formatGo !== 'function' && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 200)); // 增加等待时间
retries++;
if (retries % 5 === 0) {
console.log(`Waiting for formatGo function... (${retries}/${maxRetries})`);
}
}
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function not available after 20 retries');
}
console.log('Go WASM initialization completed successfully');
})();
/**
* Initializes the Go WebAssembly module for formatting Go code.
* This function sets up the WASM runtime and makes the formatGo function
* available on the global object.
*
* @async
* @function initialize
* @returns {Promise<void>} A promise that resolves when the WASM module is ready
* @throws {Error} If the WASM file cannot be loaded or instantiated
*/
function initialize() {
if (initializePromise) {
return initializePromise;
};
}
export const languages = [
{
name: "Go",
parsers: ["go-format"],
extensions: [".go"],
vscodeLanguageIds: ["go"],
},
initializePromise = (async () => {
const go = new TinyGo();
// Load WASM file from browser
const response = await fetch('/go-format.wasm');
if (!response.ok) {
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
}
const wasmBuffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(
wasmBuffer,
go.importObject
);
// go.run returns a promise that resolves when the go program exits.
// Since our program is a long-running service (it exposes a function and waits),
// we don't await it.
go.run(instance);
// The `formatGo` function is now available on the global object.
})();
return initializePromise;
}
/**
* Prettier language configuration for Go.
* Defines the language settings, file extensions, and parser mappings.
*
* @type {Array<Object>}
* @property {string} name - The display name of the language
* @property {string[]} parsers - Array of parser names for this language
* @property {string[]} extensions - File extensions associated with this language
* @property {string[]} vscodeLanguageIds - VSCode language identifier mappings
*/
const languages = [
{
name: "Go",
parsers: ["go-format"],
extensions: [".go"],
vscodeLanguageIds: ["go"],
},
];
export const parsers = {
"go-format": {
parse: (text) => text,
astFormat: "go-format",
locStart: (node) => 0,
locEnd: (node) => node.length,
/**
* Prettier parser configuration for Go.
* Defines how Go source code should be parsed and processed.
*
* @type {Object<string, Object>}
* @property {Object} go-format - Go language parser configuration
* @property {Function} go-format.parse - Parser function that returns the input text as-is
* @property {string} go-format.astFormat - AST format identifier for the printer
* @property {Function} go-format.locStart - Function to get the start location of a node
* @property {Function} go-format.locEnd - Function to get the end location of a node
*/
const parsers = {
"go-format": {
/**
* Parse Go source code. For this plugin, we pass through the text as-is
* since the actual formatting is handled by the Go WASM module.
*
* @param {string} text - The Go source code to parse
* @returns {string} The input text unchanged
*/
parse: (text) => text,
astFormat: "go-format",
// These are required for Prettier to work
/**
* Get the start location of a node in the source code.
*
* @param {string} node - The node (in this case, the source text)
* @returns {number} Always returns 0 as we treat the entire text as one node
*/
locStart: (node) => 0,
/**
* Get the end location of a node in the source code.
*
* @param {string} node - The node (in this case, the source text)
* @returns {number} The length of the text
*/
locEnd: (node) => node.length,
},
};
/**
* Prettier printer configuration for Go.
* Defines how the parsed Go AST should be formatted back to text.
*
* @type {Object<string, Object>}
* @property {Object} go-format - Go formatting printer configuration
* @property {Function} go-format.print - Async function that formats Go code
*/
const printers = {
"go-format": {
/**
* Format Go source code using the WebAssembly Go formatter.
* This function initializes the WASM module if needed and calls the
* global formatGo function exposed by the Go program.
*
* @async
* @param {Object} path - Prettier's path object containing the source code
* @param {Function} path.getValue - Function to get the current node value
* @returns {Promise<string>} The formatted Go source code
* @throws {Error} If the WASM module fails to initialize or format the code
*/
print: async (path) => {
// The WASM module must be initialized before we can format.
await initialize();
const text = path.getValue();
// The `formatGo` function is exposed on the global object by our Go program.
return globalThis.formatGo(text);
},
},
};
export const printers = {
"go-format": {
print: (path) => {
const text = path.getValue();
if (typeof globalThis.formatGo !== 'function') {
// 如果 formatGo 函数不可用,尝试初始化
initialize().then(() => {
// 初始化完成后formatGo 应该可用
}).catch(err => {
console.error('Go WASM initialization failed:', err);
});
// 如果还是不可用,返回原始文本
return text;
}
try {
return globalThis.formatGo(text);
} catch (error) {
console.error('Go formatting failed:', error);
// 返回原始文本而不是抛出错误
return text;
}
},
},
};
// Export initialize function for manual initialization
export { initialize };
// Default export for Prettier plugin compatibility
export default {
languages,
parsers,
printers
};
export default { languages, parsers, printers, initialize };

View File

@@ -4,8 +4,8 @@
// functionality for the Prettier plugin. This package exposes the formatGo function
// to JavaScript, enabling web-based Go code formatting using Go's built-in format package.
//
// The module is designed to be compiled to WebAssembly and loaded in Node.js
// environments as part of the go-prettier-format plugin.
// The module is designed to be compiled to WebAssembly using native Go (GOOS=js GOARCH=wasm)
// and loaded in browser environments as part of the Go Prettier plugin.
package main
import (

View File

@@ -1,26 +1,53 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
//
// This file has been modified for use by the TinyGo compiler.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
global.fs = require("node:fs");
}
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
if (!global.fs) {
let outputBuf = "";
globalThis.fs = {
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
@@ -58,8 +85,8 @@
};
}
if (!globalThis.process) {
globalThis.process = {
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
@@ -73,58 +100,53 @@
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
if (!global.crypto) {
const nodeCrypto = require("node:crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
if (!global.TextEncoder) {
global.TextEncoder = require("node:util").TextEncoder;
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
if (!global.TextDecoder) {
global.TextDecoder = require("node:util").TextDecoder;
}
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
globalThis.Go = class {
global.TinyGo = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.memory.buffer);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
const unboxValue = (v_ref) => {
reinterpretBuf.setBigInt64(0, v_ref, true);
const f = reinterpretBuf.getFloat64(0, true);
if (f === 0) {
return undefined;
}
@@ -132,69 +154,77 @@
return f;
}
const id = this.mem.getUint32(addr, true);
const id = v_ref & 0xffffffffn;
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
const loadValue = (addr) => {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
return nanHead << 32n;
}
this.mem.setFloat64(addr, v, true);
return;
if (v === 0) {
return (nanHead << 32n) | 1n;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
switch (v) {
case undefined:
return 0n;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
id = BigInt(this._values.length);
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
let typeFlag = 1n;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
typeFlag = 2n;
break;
case "symbol":
typeFlag = 3;
typeFlag = 3n;
break;
case "function":
typeFlag = 4;
typeFlag = 4n;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
return id | ((nanHead | typeFlag) << 32n);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
const storeValue = (addr, v) => {
let v_ref = boxValue(v);
mem().setBigUint64(addr, v_ref, true);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const loadSlice = (array, len, cap) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
}
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
@@ -202,347 +232,287 @@
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
const loadString = (ptr, len) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
wasi_snapshot_preview1: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
// func ticks() int64
"runtime.ticks": () => {
return BigInt((timeOrigin + performance.now()) * 1e6);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
// func sleepTicks(timeout int64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(() => {
if (this.exited) return;
try {
this._inst.exports.go_scheduler();
} catch (e) {
if (e !== wasmExit) throw e;
}
}, Number(timeout)/1e6);
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
"syscall/js.finalizeRef": (v_ref) => {
// Note: TinyGo does not support finalizers so this is only called
// for one specific case, by js.go:jsString. and can/might leak memory.
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
"syscall/js.stringVal": (value_ptr, value_len) => {
value_ptr >>>= 0;
const s = loadString(value_ptr, value_len);
return boxValue(s);
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
let prop = loadString(p_ptr, p_len);
let v = unboxValue(v_ref);
let result = Reflect.get(v, prop);
return boxValue(result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
"syscall/js.valueIndex": (v_ref, i) => {
return boxValue(Reflect.get(unboxValue(v_ref), i));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
const m = Reflect.get(v, name);
storeValue(ret_addr, Reflect.apply(m, v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
storeValue(ret_addr, Reflect.apply(v, undefined, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
storeValue(ret_addr, Reflect.construct(v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
storeValue(ret_addr, err);
mem().setUint8(ret_addr+ 8, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
"syscall/js.valueLength": (v_ref) => {
return unboxValue(v_ref).length;
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
const s = String(unboxValue(v_ref));
const str = encoder.encode(s);
storeValue(ret_addr, str);
mem().setInt32(ret_addr + 8, str.length, true);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
const str = unboxValue(v_ref);
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
return unboxValue(v_ref) instanceof unboxValue(t_ref);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
// copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified:
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = unboxValue(dst_ref);
const src = loadSlice(src_addr, src_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
}
};
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
// For compatibility, we use both as long as Go 1.20 is supported.
this.importObject.env = this.importObject.gojs;
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
global,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this.exitCode = 0;
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
if (this._inst.exports._start) {
let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
// Run program, but catch the wasmExit exception that's thrown
// to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
await exitPromise;
return this.exitCode;
} else {
this._inst.exports._initialize();
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
try {
this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) {
this._resolveExitPromise();
}
@@ -558,4 +528,26 @@
};
}
}
if (
global.require &&
global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
let exitCode = await go.run(result.instance);
process.exit(exitCode);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})();

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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 });
}
}

View File

@@ -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;

View File

@@ -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
};
}
}

View File

@@ -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)) {
// 继续跳过
}
}
}

View File

@@ -46,8 +46,8 @@ import * as shellPrettierPlugin from "@/common/prettier/plugins/shell";
import tomlPrettierPlugin from "@/common/prettier/plugins/toml";
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
import powershellPrettierPlugin from "@/common/prettier/plugins/powershell";
import scalaPrettierPlugin from "@/common/prettier/plugins/scala";
import clangPrettierPlugin from "@/common/prettier/plugins/clang";
import * as prettierPluginEstree from "prettier/plugins/estree";
/**
@@ -103,7 +103,10 @@ export const LANGUAGES: LanguageInfo[] = [
parser: "xml",
plugins: [xmlPrettierPlugin]
}),
new LanguageInfo("cpp", "C++", cppLanguage.parser),
new LanguageInfo("cpp", "C++", cppLanguage.parser,{
parser: "clang",
plugins: [clangPrettierPlugin]
}),
new LanguageInfo("rs", "Rust", rustLanguage.parser,{
parser: "jinx-rust",
plugins: [rustPrettierPlugin]
@@ -146,10 +149,7 @@ export const LANGUAGES: LanguageInfo[] = [
parser: "groovy",
plugins: [groovyPrettierPlugin]
}),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser,{
parser: "powershell",
plugins: [powershellPrettierPlugin]
}),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
new LanguageInfo("dart", "Dart", null), // 暂无解析器
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser,{
parser: "scala",

2
go.mod
View File

@@ -1,6 +1,6 @@
module voidraft
go 1.24.4
go 1.25
require (
github.com/creativeprojects/go-selfupdate v1.5.0