package jsplugin import ( "context" "encoding" "encoding/json" "errors" "fmt" "maps" "math/big" "reflect" "regexp" "slices" "strings" "sync" "time" "unsafe" "github.com/Calcium-Ion/moejs" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" ) const ( DefaultCallTimeout = 5 * time.Second DefaultConcurrency = 8 ) var ErrCallAdmissionTimeout = errors.New("plugin call admission timed out") const hookErrorMessageLimit = 512 // HookError reports a JavaScript exception thrown by a plugin hook. Message // is the sanitized JS error message with engine prefixes stripped; it is safe // to surface to API callers. type HookError struct { Hook string Message string wrapped error } func (e *HookError) Error() string { if e == nil || e.wrapped == nil { return "plugin hook failed" } return e.wrapped.Error() } func (e *HookError) Unwrap() error { if e == nil { return nil } return e.wrapped } // ResultError reports a hook result that CallInto could not store: one the // host codec cannot encode (a NaN, an infinity, a cycle) or one the target's // type rejects. Its message is the codec's. type ResultError struct { err error } func (e *ResultError) Error() string { return e.err.Error() } func (e *ResultError) Unwrap() error { return e.err } func newHookError(hook, rawMessage string, wrapped error) *HookError { var b strings.Builder b.Grow(len(rawMessage)) count := 0 for _, r := range rawMessage { if count >= hookErrorMessageLimit { break } if r < 0x20 || (r >= 0x80 && r <= 0x9F) { r = ' ' } b.WriteRune(r) count++ } message := b.String() if message == "" { message = "plugin hook failed" } return &HookError{Hook: hook, Message: message, wrapped: wrapped} } var forbiddenSyntax = regexp.MustCompile(`(?m)(^|[^A-Za-z0-9_$])(async|await|import)([^A-Za-z0-9_$]|$)`) type Options struct { Key string Version string Timeout time.Duration Concurrency int Now func() time.Time Log func(string) } type Engine struct { key string version string timeout time.Duration now func() time.Time log func(string) module *moejs.Module pool chan *runtimeInstance semaphore chan struct{} // parked bounds the runtimes that lent their slot to other calls while // utils.fetch waits on the network; its capacity is the concurrency. parked chan struct{} hooksMu sync.RWMutex hooks map[hookKey]moejs.Hook // exports holds the exports that were not undefined once the module // loaded; it is written only by Compile. exports map[string]struct{} } // hookKey names a cached hook handle. Contract hooks are an export and at // most two members (protocols..); longer paths only come from // fixture requests and are resolved on every call. type hookKey struct { export string members [2]string depth int } // Fixture requests name arbitrary members, so the handle cache is bounded. const maxCachedHooks = 256 type runtimeInstance struct { runtime *moejs.Runtime logContext *runtimeLogContext // call is the hook call running on the runtime, nil between calls. call *runtimeCall } type runtimeLogContext struct { context context.Context } // Compile performs upload-time syntax checks and compiles an ESM plugin once. // All engine-specific module and runtime handling is intentionally kept here. func Compile(source string, options Options) (*Engine, error) { if match := forbiddenSyntax.FindString(sourceWithoutCommentsAndStrings(source)); match != "" { return nil, fmt.Errorf("unsupported plugin syntax %q: plugins must be synchronous and cannot import modules", strings.TrimSpace(match)) } // The compiler never reads files, so a sourceMappingURL comment in // untrusted source stays inert. module, err := moejs.Compile(options.Key+".js", source) if err != nil { return nil, fmt.Errorf("compile plugin: %w", err) } // export ... from has no import keyword; the host links no modules. if requests := module.Requests(); len(requests) > 0 { return nil, fmt.Errorf("unsupported plugin syntax: re-export from %q: plugins must be synchronous and cannot import modules", requests[0]) } timeout := options.Timeout if timeout <= 0 { timeout = DefaultCallTimeout } concurrency := options.Concurrency if concurrency <= 0 { concurrency = DefaultConcurrency } now := options.Now if now == nil { now = time.Now } engine := &Engine{ key: options.Key, version: options.Version, timeout: timeout, now: now, log: options.Log, module: module, semaphore: make(chan struct{}, concurrency), parked: make(chan struct{}, concurrency), pool: make(chan *runtimeInstance, concurrency), hooks: make(map[hookKey]moejs.Hook), exports: make(map[string]struct{}), } instance, err := engine.newRuntime(context.Background()) if err != nil { return nil, err } for _, name := range module.Exports() { if value, found := instance.runtime.Export(name); found && !value.IsUndefined() { engine.exports[name] = struct{}{} } } instance.logContext.context = nil engine.putRuntime(instance) return engine, nil } // Export returns one module export without exposing engine values outside the // engine boundary. It is used for declarative exports such as meta. func (e *Engine) Export(ctx context.Context, exportName string) (any, error) { select { case e.semaphore <- struct{}{}: defer func() { <-e.semaphore }() case <-ctx.Done(): return nil, ctx.Err() } instance, err := e.getRuntime(ctx) if err != nil { return nil, err } reusable := true defer func() { instance.runtime.ClearInterrupt() instance.logContext.context = nil if reusable { e.putRuntime(instance) } }() timedOut := errors.New("plugin export timed out") watchdog := watchRuntimeContext(instance.runtime, ctx, e.timeout, timedOut) defer watchdog.stop() value, found := instance.runtime.Export(exportName) if !found || value.IsUndefined() { return nil, fmt.Errorf("plugin export %q not found", exportName) } result, err := instance.runtime.ToGo(value) if err == nil { return result, nil } reusable = false var interrupted *moejs.InterruptedError if errors.As(err, &interrupted) { return nil, fmt.Errorf("plugin %s@%s export %s interrupted: %w", e.key, e.version, exportName, interruptCause(interrupted)) } var exception *moejs.Exception errors.As(err, &exception) return nil, fmt.Errorf("plugin %s@%s export %s failed: %w%s", e.key, e.version, exportName, err, thrownAt(instance.runtime, exception)) } // HasExport reports whether a module export was defined when the plugin // loaded: an export that was undefined then counts as absent, and a later // assignment is not seen. Optional contract hooks should be detected with // this method instead of relying on engine errors. It runs no JavaScript, so // it neither waits for nor takes a call slot. func (e *Engine) HasExport(exportName string) bool { _, found := e.exports[exportName] return found } // HasCallablePath reports whether an exported value, or a nested member below // it, exists and is callable. func (e *Engine) HasCallablePath(ctx context.Context, exportName string, members ...string) (bool, error) { select { case e.semaphore <- struct{}{}: defer func() { <-e.semaphore }() case <-ctx.Done(): return false, ctx.Err() } hook, err := e.hook(exportName, members) if err != nil { return false, nil } instance, err := e.getRuntime(ctx) if err != nil { return false, err } reusable := true defer func() { instance.runtime.ClearInterrupt() instance.logContext.context = nil if reusable { e.putRuntime(instance) } }() timedOut := errors.New("plugin inspection timed out") watchdog := watchRuntimeContext(instance.runtime, ctx, e.timeout, timedOut) defer watchdog.stop() found, err := instance.runtime.Has(hook) if err == nil { return found, nil } reusable = false var interrupted *moejs.InterruptedError if errors.As(err, &interrupted) { return false, fmt.Errorf("plugin %s@%s hook %s inspection interrupted: %w", e.key, e.version, hook.Name(), interruptCause(interrupted)) } var exception *moejs.Exception errors.As(err, &exception) return false, fmt.Errorf("plugin %s@%s hook %s inspection failed: %w%s", e.key, e.version, hook.Name(), err, thrownAt(instance.runtime, exception)) } // RawJSON is a hook argument given as JSON text: the hook receives JSON.parse // of it, members in order, whether it is an argument or inside one, with no Go // values built in between; text nested too deep for JSON.parse is decoded with // the host codec instead. The engine parses the bytes in place, without the // copy ParseJSON makes, and strings of the call's result may share them, so // the caller must never change them once passed. type RawJSON []byte // Call invokes one named module export and returns its JSON-compatible value. func (e *Engine) Call(ctx context.Context, exportName string, args ...any) (result any, err error) { result, _, err = e.call(ctx, callOptions{}, exportName, nil, args...) return result, err } // CallInto invokes one named module export and stores its result in target as // the host codec decodes the encoding of the value Call returns, without that // round trip for plain results. A result the codec cannot encode or target's // type rejects is a *ResultError, and target may then hold part of it. func (e *Engine) CallInto(ctx context.Context, target any, exportName string, args ...any) error { _, _, err := e.call(ctx, callOptions{target: target}, exportName, nil, args...) return err } // CallJSONInto is CallInto with the semantics of JSON.stringify's text of the // result instead of the host codec's encoding: objects keep their member // order, which a json.RawMessage in target receives as text, members whose // value is undefined are left out and toJSON runs. func (e *Engine) CallJSONInto(ctx context.Context, target any, exportName string, args ...any) error { _, _, err := e.call(ctx, callOptions{target: target, stringify: true}, exportName, nil, args...) return err } // CallMember invokes a function stored on an exported object, such as a // renderer in the renderers export. func (e *Engine) CallMember(ctx context.Context, exportName, memberName string, args ...any) (result any, err error) { result, _, err = e.call(ctx, callOptions{}, exportName, []string{memberName}, args...) return result, err } // CallPath invokes a function nested below an exported object. It is used for // protocol hooks such as protocols.openai_responses.renderEvents. func (e *Engine) CallPath(ctx context.Context, exportName string, members []string, args ...any) (result any, err error) { result, _, err = e.call(ctx, callOptions{}, exportName, members, args...) return result, err } // CallPathInto is CallInto for a function nested below an exported object. func (e *Engine) CallPathInto(ctx context.Context, target any, exportName string, members []string, args ...any) error { _, _, err := e.call(ctx, callOptions{target: target}, exportName, members, args...) return err } // CallPathWithAdmissionTimeout gives long-lived observers a separate bound for // waiting on JavaScript capacity. Once admitted, the hook receives the // engine's full execution timeout instead of inheriting time already spent in // the semaphore queue. func (e *Engine) CallPathWithAdmissionTimeout( ctx context.Context, admissionTimeout time.Duration, exportName string, members []string, args ...any, ) (result any, err error) { result, _, err = e.call(ctx, callOptions{admissionTimeout: admissionTimeout}, exportName, members, args...) return result, err } // CallPathWithMemberJSON is CallPathWithAdmissionTimeout that also returns the // result's member as the text JSON.stringify writes for it, which keeps the // member order of its objects. The text is nil when member is empty or the // result has no such member. func (e *Engine) CallPathWithMemberJSON( ctx context.Context, admissionTimeout time.Duration, member string, exportName string, members []string, args ...any, ) (result any, text json.RawMessage, err error) { return e.call(ctx, callOptions{admissionTimeout: admissionTimeout, jsonMember: member}, exportName, members, args...) } // callOptions is what a hook call does besides calling: the bound on waiting // for a JavaScript slot, the Go value the result is stored in instead of // being returned (from its JSON.stringify text when stringify is set), and a // member of the result also returned as JSON text. type callOptions struct { admissionTimeout time.Duration target any stringify bool jsonMember string } func (e *Engine) call(ctx context.Context, opts callOptions, exportName string, members []string, args ...any) (any, json.RawMessage, error) { if err := e.acquireCallSlot(ctx, opts.admissionTimeout); err != nil { return nil, nil, err } call := &runtimeCall{engine: e, ctx: ctx, holdsSlot: true} // utils.fetch may lend the slot out; return what the call holds now. defer call.release() hook, err := e.hook(exportName, members) if err != nil { if len(members) == 0 { return nil, nil, fmt.Errorf("plugin export %q not found", exportName) } return nil, nil, fmt.Errorf("plugin hook %q not found", exportName) } instance, err := e.getRuntime(ctx) if err != nil { return nil, nil, err } call.runtime, call.hook = instance.runtime, hook.Name() call.fetcher, _ = ctx.Value(fetcherContextKey{}).(Fetcher) instance.call = call reusable := true defer func() { instance.runtime.ClearInterrupt() instance.logContext.context = nil instance.call = nil if reusable { // An idle pooled runtime must not keep this call's request // data alive. instance.runtime.ReleaseCallData() e.putRuntime(instance) } }() callArgs := make([]moejs.Value, 0, 4) for index, arg := range args { var converted moejs.Value if raw, ok := arg.(RawJSON); !ok { value, _ := pluginValue(instance.runtime, arg, 0) converted, err = instance.runtime.FromGo(value) } else if converted, err = instance.runtime.ParseJSONString(unsafe.String(unsafe.SliceData(raw), len(raw))); err != nil { // JSON.parse stops at a lower nesting depth than the host codec. var decoded any if err = common.Unmarshal(raw, &decoded); err == nil { converted, err = instance.runtime.FromGo(decoded) } } if err != nil { return nil, nil, fmt.Errorf("plugin %s@%s hook %s argument %d: %w", e.key, e.version, hook.Name(), index+1, err) } callArgs = append(callArgs, converted) } timedOut := errors.New("plugin call timed out") call.watchdog = watchRuntimeContext(instance.runtime, ctx, e.timeout, timedOut) // utils.fetch pauses and restarts the watchdog; stop whichever watch is // current when the call ends. defer call.watchdog.stop() value, err := instance.runtime.Call(hook, callArgs...) var interrupted *moejs.InterruptedError var exception *moejs.Exception if err == nil { var result any var memberText json.RawMessage switch { case opts.target != nil && opts.stringify: err = instance.runtime.Unmarshal(value, opts.target) case opts.target != nil: err = instance.runtime.ToGoInto(value, opts.target) default: result, err = instance.runtime.ToGo(value) } if err == nil && opts.jsonMember != "" { // After the result's Go value: AppendJSON may run a toJSON or // a getter, which ToGo never does. var member moejs.Value if member, err = instance.runtime.Get(value, opts.jsonMember); err == nil && !member.IsUndefined() { memberText, err = instance.runtime.AppendJSON(nil, member) } } if err == nil { return result, memberText, nil } if errors.As(err, &interrupted) { reusable = false return nil, nil, fmt.Errorf("plugin %s@%s hook %s interrupted: %w", e.key, e.version, hook.Name(), interruptCause(interrupted)) } // Past ToGo's errors, ToGoInto fails as json.Marshal and // json.Unmarshal do, which is the host codec (common/json.go), and // Unmarshal and AppendJSON as JSON.stringify does (a BigInt, a cycle). var internal *moejs.InternalError if (opts.target != nil || opts.jsonMember != "") && !errors.As(err, &exception) && !errors.As(err, &internal) { return nil, nil, &ResultError{err: err} } } switch { case errors.Is(err, moejs.ErrHookNotFound): if len(members) == 0 { return nil, nil, fmt.Errorf("plugin export %q not found", exportName) } // Name the path up to the first missing member, as far as data // properties show it; getters on the path are not run again. hookName := exportName current, found := instance.runtime.Export(exportName) for _, member := range members { if !found || current.IsNullish() { break } hookName += "." + member if !current.IsObject() { break } current, found = current.AsObject().GetOwnDataValue(instance.runtime.Realm().KeyFromGoString(member)) } return nil, nil, fmt.Errorf("plugin hook %q not found", hookName) case errors.Is(err, moejs.ErrNotCallable): return nil, nil, fmt.Errorf("plugin hook %q is not a function", hook.Name()) case errors.As(err, &interrupted): reusable = false return nil, nil, fmt.Errorf("plugin %s@%s hook %s failed: %w", e.key, e.version, hook.Name(), interruptCause(interrupted)) case errors.As(err, &exception): wrapped := fmt.Errorf("plugin %s@%s hook %s failed: %w%s", e.key, e.version, hook.Name(), err, thrownAt(instance.runtime, exception)) return nil, nil, newHookError(hook.Name(), exception.Message(), wrapped) } reusable = false return nil, nil, fmt.Errorf("plugin %s@%s hook %s failed: %w", e.key, e.version, hook.Name(), err) } // hook resolves a hook path against the module's export table. Only // declared exports resolve; the handle is cached, while the bindings it // names are read live on every call. func (e *Engine) hook(exportName string, members []string) (moejs.Hook, error) { key := hookKey{export: exportName, depth: len(members)} cacheable := len(members) <= len(key.members) if cacheable { copy(key.members[:], members) e.hooksMu.RLock() hook, found := e.hooks[key] e.hooksMu.RUnlock() if found { return hook, nil } } hook, err := e.module.Hook(exportName, members...) if err != nil || !cacheable { return hook, err } e.hooksMu.Lock() if len(e.hooks) < maxCachedHooks { e.hooks[key] = hook } e.hooksMu.Unlock() return hook, nil } func (e *Engine) acquireCallSlot(ctx context.Context, admissionTimeout time.Duration) error { if admissionTimeout <= 0 { select { case e.semaphore <- struct{}{}: return nil case <-ctx.Done(): return ctx.Err() } } timer := time.NewTimer(admissionTimeout) defer timer.Stop() select { case e.semaphore <- struct{}{}: return nil case <-ctx.Done(): return ctx.Err() case <-timer.C: if err := ctx.Err(); err != nil { return err } return fmt.Errorf("%w: plugin %s@%s", ErrCallAdmissionTimeout, e.key, e.version) } } // interruptCause is what stopped a runtime: the context cause // watchRuntimeContext interrupted it with, so errors.Is sees a caller's // cancellation through the host error. func interruptCause(interrupted *moejs.InterruptedError) error { if cause, ok := interrupted.Value.(error); ok { return cause } return interrupted } // thrownAt is the frame an Error was thrown from, as " at fn (file:line:col)", // for host logs; it is empty for thrown values that are not Errors and for // errors that are not exceptions (exception is nil). func thrownAt(runtime *moejs.Runtime, exception *moejs.Exception) string { _, frames, found := strings.Cut(runtime.StackTrace(exception), "\n at ") if !found { return "" } frame, _, _ := strings.Cut(frames, "\n") return " at " + frame } // maxPluginValueDepth bounds the argument walk; deeper values, which no // host-built argument reaches, are passed on as they are. const maxPluginValueDepth = 512 // pluginValueContainers are the container types moejs converts; named types // with one of these underlying types are only retyped. var pluginValueContainers = []reflect.Type{ reflect.TypeFor[map[string]any](), reflect.TypeFor[map[string]string](), reflect.TypeFor[map[string][]string](), reflect.TypeFor[[]any](), reflect.TypeFor[[]string](), reflect.TypeFor[[]map[string]any](), } // pluginValue returns a hook argument in the shapes moejs converts: nil, // booleans, numbers, strings, JSON-shaped maps and slices and engine values. // A RawJSON inside the argument becomes rt's parse of it (RawJSON); other Go // values (structs, pointers, maps and slices of other element types) take // their JSON form through the configured codec, and named scalars and // containers become their underlying type. changed is false when v has // supported shapes throughout, the usual case, so an argument is passed on // without a copy. func pluginValue(rt *moejs.Runtime, v any, depth int) (converted any, changed bool) { if depth > maxPluginValueDepth { return v, false } switch typed := v.(type) { case nil, bool, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, json.Number, *big.Int, []byte, []string, map[string]string, map[string][]string, moejs.Value, moejs.NativeFunc: return v, false case RawJSON: if parsed, err := rt.ParseJSONString(unsafe.String(unsafe.SliceData(typed), len(typed))); err == nil { return parsed, true } // JSON.parse stops at a lower nesting depth than the host codec. var decoded any if err := common.Unmarshal(typed, &decoded); err != nil { return v, false } return decoded, true case map[string]any: var copied map[string]any for key, item := range typed { if item, changed = pluginValue(rt, item, depth+1); changed { if copied == nil { copied = maps.Clone(typed) } copied[key] = item } } if copied == nil { return v, false } return copied, true case []any: var copied []any for index, item := range typed { if item, changed = pluginValue(rt, item, depth+1); changed { if copied == nil { copied = slices.Clone(typed) } copied[index] = item } } if copied == nil { return v, false } return copied, true case []map[string]any: var copied []map[string]any for index, item := range typed { if next, changed := pluginValue(rt, item, depth+1); changed { if copied == nil { copied = slices.Clone(typed) } copied[index] = next.(map[string]any) } } if copied == nil { return v, false } return copied, true } _, marshaler := v.(json.Marshaler) _, textMarshaler := v.(encoding.TextMarshaler) if !marshaler && !textMarshaler { value := reflect.ValueOf(v) switch value.Kind() { case reflect.Bool: return value.Bool(), true case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return value.Int(), true case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return value.Uint(), true case reflect.Float32, reflect.Float64: return value.Float(), true case reflect.String: return value.String(), true case reflect.Map, reflect.Slice: for _, container := range pluginValueContainers { if value.Type().ConvertibleTo(container) { converted, _ = pluginValue(rt, value.Convert(container).Interface(), depth) return converted, true } } } } data, err := common.Marshal(v) if err != nil { return v, false } var decoded any if err = common.Unmarshal(data, &decoded); err != nil { return v, false } return decoded, true } // Idle runtimes are bounded by the execution limit and survive GC. Start with // one instance and grow only when concurrent work actually needs more. func (e *Engine) putRuntime(instance *runtimeInstance) { select { case e.pool <- instance: default: } } func (e *Engine) getRuntime(ctx context.Context) (*runtimeInstance, error) { select { case instance := <-e.pool: instance.logContext.context = ctx return instance, nil default: return e.newRuntime(ctx) } } // runtimeWatchdog interrupts a runtime with timeoutError once it has run for // its timeout, or with the context's cause once ctx ends. utils.fetch pauses // it, so the timeout counts JavaScript time only. A timeout callback must // finish before its runtime can be reused: merely stopping a timer does not // wait for an already-started Interrupt call. type runtimeWatchdog struct { runtime *moejs.Runtime ctx context.Context timeoutError error remaining time.Duration started time.Time watch context.Context stopWatch func() bool cancel context.CancelFunc interrupted chan struct{} paused bool } func watchRuntimeContext(runtime *moejs.Runtime, ctx context.Context, timeout time.Duration, timeoutError error) *runtimeWatchdog { watchdog := &runtimeWatchdog{runtime: runtime, ctx: ctx, timeoutError: timeoutError, remaining: timeout} watchdog.start() return watchdog } func (w *runtimeWatchdog) start() { watch, cancel := context.WithTimeoutCause(w.ctx, w.remaining, w.timeoutError) interrupted := make(chan struct{}) w.stopWatch = context.AfterFunc(watch, func() { w.runtime.Interrupt(context.Cause(watch)) close(interrupted) }) w.watch, w.cancel, w.interrupted = watch, cancel, interrupted w.started, w.paused = time.Now(), false } func (w *runtimeWatchdog) stop() { if w.paused { return } if !w.stopWatch() { <-w.interrupted } w.cancel() } // pause stops the clock while a host function waits. It reports false when // the watchdog already fired: its interrupt is then ending the call, and the // host function must return instead of waiting. func (w *runtimeWatchdog) pause() bool { if !w.stopWatch() { <-w.interrupted return false } w.cancel() w.remaining -= time.Since(w.started) w.paused = true return true } // resume restarts the clock with the time left at pause and the same timeout // cause. When the time is up or ctx ended meanwhile, it waits for the // interrupt that follows, so the host function can return it. func (w *runtimeWatchdog) resume() { w.start() if w.watch.Err() != nil { <-w.interrupted } } func (e *Engine) newRuntime(ctx context.Context) (*runtimeInstance, error) { // Code compiled from strings would bypass the upload-time syntax checks, // so eval and the Function constructors throw an EvalError. runtime := moejs.NewRuntime(moejs.Options{DisableDynamicCode: true}) logContext := &runtimeLogContext{context: ctx} instance := &runtimeInstance{runtime: runtime, logContext: logContext} logOutput := e.log if logOutput == nil { logOutput = func(message string) { logger.LogDebug(logContext.context, "task_plugin subsystem=runtime event=console message=%q", message) } } if err := injectGlobals(runtime, func() string { return fmt.Sprintf("[plugin:%s@%s]", e.key, e.version) }, e.now, logOutput, func() *runtimeCall { return instance.call }); err != nil { return nil, fmt.Errorf("inject plugin utils: %w", err) } timedOut := errors.New("plugin initialization timed out") watchdog := watchRuntimeContext(runtime, ctx, e.timeout, timedOut) err := runtime.Load(e.module) watchdog.stop() runtime.ClearInterrupt() var interrupted *moejs.InterruptedError if errors.As(err, &interrupted) { return nil, fmt.Errorf("initialize plugin %s@%s: %w", e.key, e.version, interruptCause(interrupted)) } if err != nil { return nil, fmt.Errorf("evaluate plugin: %w", err) } return instance, nil } func sourceWithoutCommentsAndStrings(source string) string { var output strings.Builder output.Grow(len(source)) quote := byte(0) escaped := false lineComment := false blockComment := false for i := 0; i < len(source); i++ { current := source[i] next := byte(0) if i+1 < len(source) { next = source[i+1] } if lineComment { if current == '\n' { lineComment = false output.WriteByte('\n') } else { output.WriteByte(' ') } continue } if blockComment { if current == '*' && next == '/' { blockComment = false output.WriteString(" ") i++ } else { output.WriteByte(' ') } continue } if quote != 0 { output.WriteByte(' ') if escaped { escaped = false } else if current == '\\' { escaped = true } else if current == quote { quote = 0 } continue } if current == '/' && next == '/' { lineComment = true output.WriteString(" ") i++ continue } if current == '/' && next == '*' { blockComment = true output.WriteString(" ") i++ continue } if current == '\'' || current == '"' || current == '`' { quote = current output.WriteByte(' ') continue } output.WriteByte(current) } return output.String() }