mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-13 02:23:10 +08:00
Merge main
This commit is contained in:
@@ -2265,14 +2265,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrExecFileFn fn) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
@@ -4905,14 +4905,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrExecFileFn fn) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
|
||||
@@ -155,11 +155,9 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
|
||||
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
|
||||
/* The callback slice: file + args, default options. The returned child has
|
||||
* all three stdio slots piped; stdout/stderr are captured internally and
|
||||
* the callback moves into the child registry until settlement. With shell
|
||||
* (exec), args is ["-c", command] for /bin/sh and failure messages show
|
||||
* the command, as for scr_exec_sync. */
|
||||
* the callback moves into the child registry until settlement. */
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
|
||||
ScrExecFileFn fn, bool shell);
|
||||
ScrExecFileFn fn);
|
||||
ScrChild *scr_child_retain(ScrChild *c);
|
||||
void scr_child_release(ScrChild *c);
|
||||
void *scr_child_retain_v(void *p);
|
||||
|
||||
@@ -603,6 +603,18 @@ ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */
|
||||
ScrSym *scr_sym_well_known(ScrStr *name);
|
||||
ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key);
|
||||
ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional);
|
||||
/* `using` and `await using` with a dyn value. scr_dyn_using_method runs the
|
||||
* declaration's checks: null and undefined answer undefined (nothing to
|
||||
* dispose); any other value that is not an object or function throws
|
||||
* Node's TypeError, as does a missing or non-callable [Symbol.dispose].
|
||||
* With `async`, [Symbol.asyncDispose] is read first and [Symbol.dispose]
|
||||
* is the fallback, and the answer records which one was found.
|
||||
* scr_dyn_using_dispose calls that answer with `value` as `this` at scope
|
||||
* exit and returns the call's result (+1; undefined for a sync fallback,
|
||||
* whose result `await using` does not await). Both borrow their operands
|
||||
* and return NULL with the exception pending. */
|
||||
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async);
|
||||
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value);
|
||||
void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
|
||||
void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key);
|
||||
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key);
|
||||
|
||||
@@ -33,7 +33,8 @@ static const char *const scr_error_names[SCR_ERR_COUNT] = {
|
||||
/* DOMException's DEFAULT name is "Error" too (WebIDL: the name
|
||||
* parameter defaults to "Error"); the class identity lives in the
|
||||
* vtable, never the name. */
|
||||
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError"};
|
||||
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError",
|
||||
"SuppressedError"};
|
||||
|
||||
void scr_error_trace(void *e, ScrTraceVisit visit, void *ctx) {
|
||||
/* name/message are strings — never collector-headered; nothing to visit. */
|
||||
@@ -140,6 +141,28 @@ static void scr_aggregate_destroy(void *obj) {
|
||||
scr_error_destroy(obj);
|
||||
}
|
||||
|
||||
/* SuppressedError teardown: the ScrError fields plus the disposal
|
||||
* snapshots scr_exc_suppress recorded for its `error` and `suppressed`. */
|
||||
static void scr_suppressed_gcfree(void *obj) {
|
||||
scr_exc_suppressed_forget(obj);
|
||||
scr_error_gcfree(obj);
|
||||
}
|
||||
|
||||
static void scr_suppressed_destroy(void *obj) {
|
||||
scr_exc_suppressed_forget(obj);
|
||||
scr_error_destroy(obj);
|
||||
}
|
||||
|
||||
static void scr_suppressed_reld(void *obj) {
|
||||
ScrError *e = (ScrError *)obj;
|
||||
if (--e->rc == 0) {
|
||||
if (scr_error_traced) scr_cyc_dead(e);
|
||||
scr_rc_destroy(obj, scr_suppressed_destroy);
|
||||
} else if (scr_error_traced) {
|
||||
scr_cyc_released(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_aggregate_reld(void *obj) {
|
||||
ScrAggregateError *a = (ScrAggregateError *)obj;
|
||||
if (--a->base.rc == 0) {
|
||||
@@ -163,6 +186,7 @@ SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT] = {
|
||||
{7, 7, &scr_error_reld}, /* URIError */
|
||||
{8, 8, &scr_aggregate_reld}, /* AggregateError */
|
||||
{9, 9, &scr_error_reld}, /* AssertionError */
|
||||
{10, 10, &scr_suppressed_reld}, /* SuppressedError */
|
||||
};
|
||||
|
||||
ScrError *scr_error_retain(ScrError *e) {
|
||||
@@ -208,11 +232,13 @@ static ScrError *scr_error_alloc(int kind) {
|
||||
static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
|
||||
static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType suppressed_type = { &scr_error_trace, &scr_suppressed_gcfree, SCR_CYC_KIND_ERROR };
|
||||
ScrError *e;
|
||||
if (scr_error_traced) {
|
||||
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type
|
||||
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
|
||||
: &error_type);
|
||||
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
|
||||
: kind == SCR_ERR_SUPPRESSED ? &suppressed_type
|
||||
: &error_type);
|
||||
} else {
|
||||
e = calloc(1, size);
|
||||
if (!e) scr_error_oom();
|
||||
|
||||
@@ -119,7 +119,8 @@ enum {
|
||||
SCR_ERR_URI = 7,
|
||||
SCR_ERR_AGGREGATE = 8, /* AggregateError — ScrAggregateError, the wider layout */
|
||||
SCR_ERR_ASSERTION = 9, /* node:assert's AssertionError — the Error layout */
|
||||
SCR_ERR_COUNT = 10,
|
||||
SCR_ERR_SUPPRESSED = 10, /* SuppressedError — the Error layout; error and suppressed are view properties */
|
||||
SCR_ERR_COUNT = 11,
|
||||
};
|
||||
|
||||
extern SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT]; /* indexed by SCR_ERR_*; main() stamps pre/post */
|
||||
@@ -201,6 +202,14 @@ ScrError *scr_aggregate_new(const struct ScrDyn *message, const struct ScrDyn *o
|
||||
void scr_aggregate_set_errors(ScrError *e, struct ScrDyn *errors); /* both borrowed */
|
||||
struct ScrDyn *scr_aggregate_errors(ScrError *e); /* +1 */
|
||||
|
||||
/* new SuppressedError(error, suppressed, message) (scr_json.c): the message
|
||||
* as in scr_error_new_options, then `error` and `suppressed` as
|
||||
* non-enumerable properties of the error's dyn view. All arguments are
|
||||
* borrowed (dyn undefined when absent). Returns +1, or NULL with an
|
||||
* exception pending when the message conversion throws. */
|
||||
ScrError *scr_suppressed_new(const struct ScrDyn *error, const struct ScrDyn *suppressed,
|
||||
const struct ScrDyn *message);
|
||||
|
||||
/* Switch runtime-side error allocation to collector-headered (called from
|
||||
* main() when the compiler's cycle fixpoint marks the hierarchy). */
|
||||
void scr_error_set_traced(void);
|
||||
|
||||
@@ -354,10 +354,41 @@ void scr_rethrow(const ScrCaught *c) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Explicit resource management's error merge. The full JavaScript object
|
||||
* exposes `.error` and `.suppressed`; the static catch surface currently
|
||||
* exposes Error name/message, so retain Node's externally visible identity
|
||||
* and ordering while consuming both snapshots exactly once. */
|
||||
/* Explicit resource management's error merge: a SuppressedError whose
|
||||
* `error` is the disposal error and whose `suppressed` is the body error.
|
||||
* The snapshots wait in this table for the error's dyn view, so programs
|
||||
* that never read them link no checked-dynamic code. */
|
||||
typedef struct ScrSuppressedEntry {
|
||||
const void *error_object;
|
||||
ScrCaught *error;
|
||||
ScrCaught *suppressed;
|
||||
} ScrSuppressedEntry;
|
||||
static SCR_TL ScrSuppressedEntry *scr_suppressed_entries = NULL;
|
||||
static SCR_TL size_t scr_suppressed_len = 0;
|
||||
static SCR_TL size_t scr_suppressed_cap = 0;
|
||||
|
||||
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
|
||||
const ScrCaught **suppressed) {
|
||||
for (size_t i = 0; i < scr_suppressed_len; i++) {
|
||||
if (scr_suppressed_entries[i].error_object != error_object) continue;
|
||||
*error = scr_suppressed_entries[i].error;
|
||||
*suppressed = scr_suppressed_entries[i].suppressed;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void scr_exc_suppressed_forget(const void *error_object) {
|
||||
for (size_t i = 0; i < scr_suppressed_len; i++) {
|
||||
if (scr_suppressed_entries[i].error_object != error_object) continue;
|
||||
ScrSuppressedEntry entry = scr_suppressed_entries[i];
|
||||
scr_suppressed_entries[i] = scr_suppressed_entries[--scr_suppressed_len];
|
||||
scr_caught_release(entry.error);
|
||||
scr_caught_release(entry.suppressed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void scr_exc_suppress(ScrCaught *suppressed) {
|
||||
if (suppressed->kind == SCR_EXC_GENRET) {
|
||||
/* A generator .return() is a return completion, not a thrown error.
|
||||
@@ -371,11 +402,22 @@ void scr_exc_suppress(ScrCaught *suppressed) {
|
||||
scr_caught_release(suppressed);
|
||||
return;
|
||||
}
|
||||
scr_caught_release(error);
|
||||
scr_caught_release(suppressed);
|
||||
scr_throw_error_named(
|
||||
scr_str_new("SuppressedError", sizeof("SuppressedError") - 1),
|
||||
scr_str_new("An error was suppressed during disposal", sizeof("An error was suppressed during disposal") - 1));
|
||||
static const char text[] = "An error was suppressed during disposal";
|
||||
ScrStr *message = scr_str_new(text, sizeof text - 1);
|
||||
ScrError *e = scr_error_new(SCR_ERR_SUPPRESSED, message);
|
||||
scr_str_release(message);
|
||||
if (scr_suppressed_len == scr_suppressed_cap) {
|
||||
size_t cap = scr_suppressed_cap ? scr_suppressed_cap * 2 : 4;
|
||||
ScrSuppressedEntry *entries = realloc(scr_suppressed_entries, cap * sizeof *entries);
|
||||
if (!entries) {
|
||||
fputs("scriptc: out of memory\n", stderr);
|
||||
abort();
|
||||
}
|
||||
scr_suppressed_entries = entries;
|
||||
scr_suppressed_cap = cap;
|
||||
}
|
||||
scr_suppressed_entries[scr_suppressed_len++] = (ScrSuppressedEntry){e, error, suppressed};
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post) {
|
||||
|
||||
@@ -165,8 +165,16 @@ ScrCaught *scr_caught_retain(ScrCaught *c);
|
||||
void scr_caught_release(ScrCaught *c); /* NULL-tolerant */
|
||||
void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx);
|
||||
/* Replace the pending disposal error with a SuppressedError and consume the
|
||||
* original body-error snapshot. The current exception cell must be pending. */
|
||||
* original body-error snapshot. The current exception cell must be pending.
|
||||
* The SuppressedError keeps both snapshots in a side table until its
|
||||
* teardown: scr_exc_suppressed_values answers them (borrowed) for the
|
||||
* error's `error` (the disposal error) and `suppressed` (the body error)
|
||||
* when scr_json.c builds its dyn view, and scr_exc_suppressed_forget
|
||||
* releases them. */
|
||||
void scr_exc_suppress(ScrCaught *suppressed);
|
||||
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
|
||||
const ScrCaught **suppressed);
|
||||
void scr_exc_suppressed_forget(const void *error_object);
|
||||
/* Borrows the box; re-raises the saved exception exactly (payload RETAINED
|
||||
* — the binding stays live until its scope exits). */
|
||||
void scr_rethrow(const ScrCaught *c);
|
||||
|
||||
@@ -5677,6 +5677,16 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
scr_dyn_obj_set(d, "cause", 5, scr_domex_cause((ScrError *)e));
|
||||
}
|
||||
}
|
||||
/* A disposal SuppressedError's non-enumerable `error` and `suppressed`
|
||||
* (scr_exc_suppress); a constructed one sets them on its view. */
|
||||
const ScrCaught *suppressed_error = NULL, *suppressed_value = NULL;
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_SUPPRESSED] &&
|
||||
scr_exc_suppressed_values(e, &suppressed_error, &suppressed_value)) {
|
||||
scr_dyn_obj_set(d, "error", 5, scr_caught_to_dyn(suppressed_error));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_obj_set(d, "suppressed", 10, scr_caught_to_dyn(suppressed_value));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
}
|
||||
/* AggregateError's own non-enumerable `errors` list, shared with the
|
||||
* typed read so both spellings answer the same array. */
|
||||
if (e->vt == &scr_error_vts[SCR_ERR_AGGREGATE]) {
|
||||
@@ -10888,6 +10898,24 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o
|
||||
return e;
|
||||
}
|
||||
|
||||
/* SuppressedError's `error` and `suppressed`: non-enumerable own
|
||||
* properties of the error's dyn view. Both values move in. */
|
||||
static void scr_suppressed_attach(ScrError *e, ScrDyn *error, ScrDyn *suppressed) {
|
||||
ScrDyn *view = scr_dyn_from_error(e);
|
||||
scr_dyn_obj_set(view, "error", 5, error);
|
||||
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_obj_set(view, "suppressed", 10, suppressed);
|
||||
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
|
||||
scr_dyn_release(view);
|
||||
}
|
||||
|
||||
ScrError *scr_suppressed_new(const ScrDyn *error, const ScrDyn *suppressed, const ScrDyn *message) {
|
||||
ScrError *e = scr_error_new_options(SCR_ERR_SUPPRESSED, message, NULL);
|
||||
if (!e) return NULL;
|
||||
scr_suppressed_attach(e, scr_dyn_retain((ScrDyn *)error), scr_dyn_retain((ScrDyn *)suppressed));
|
||||
return e;
|
||||
}
|
||||
|
||||
ScrDyn *scr_error_cause(ScrError *e) {
|
||||
size_t hit = scr_errdyn_find(e, true);
|
||||
if (hit != SIZE_MAX) {
|
||||
|
||||
@@ -356,3 +356,104 @@ void scr_dyn_dirent_finish(ScrDyn *obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* The well-known symbol `Symbol.<name>` as a dyn key (+1). */
|
||||
static ScrDyn *scr_using_key(const char *name, size_t len) {
|
||||
ScrStr *text = scr_str_new(name, len);
|
||||
ScrSym *symbol = scr_sym_well_known(text);
|
||||
scr_str_release(text);
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_sym_release(symbol);
|
||||
return key;
|
||||
}
|
||||
|
||||
static bool scr_using_typeof_is(const ScrDyn *value, const char *name, size_t len) {
|
||||
ScrStr *type = scr_dyn_typeof(value);
|
||||
bool match = type->len == len && !memcmp(type->data, name, len);
|
||||
scr_str_release(type);
|
||||
return match;
|
||||
}
|
||||
|
||||
/* The [Symbol.<name>] method of a `using` value: NULL for an absent member
|
||||
* (undefined or null), the callable member (+1), or NULL with a pending
|
||||
* TypeError when the member is not callable. */
|
||||
static ScrDyn *scr_using_get_method(const ScrDyn *value, const char *name, size_t len, bool *absent) {
|
||||
ScrDyn *key = scr_using_key(name, len);
|
||||
ScrDyn *method = scr_dyn_symbol_key_get(value, key, false);
|
||||
scr_dyn_release(key);
|
||||
*absent = false;
|
||||
if (!method) return NULL;
|
||||
if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) {
|
||||
scr_dyn_release(method);
|
||||
*absent = true;
|
||||
return NULL;
|
||||
}
|
||||
if (!scr_using_typeof_is(method, "function", 8)) {
|
||||
scr_dyn_release(method);
|
||||
ScrJsonBuf message;
|
||||
scr_jb_init(&message);
|
||||
scr_jb_puts(&message, "Symbol(Symbol.");
|
||||
scr_jb_write(&message, name, len);
|
||||
scr_jb_puts(&message, ") is not a function");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
|
||||
return NULL;
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async) {
|
||||
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
|
||||
if (!scr_using_typeof_is(value, "object", 6) && !scr_using_typeof_is(value, "function", 8)) {
|
||||
static const char message[] = "An object is expected with `using` declarations";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
bool absent = false;
|
||||
if (async) {
|
||||
ScrDyn *method = scr_using_get_method(value, "asyncDispose", 12, &absent);
|
||||
if (!method && !absent) return NULL;
|
||||
if (method) {
|
||||
ScrDyn *record = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(record, method);
|
||||
scr_dyn_arr_push(record, scr_dyn_new_bool(false));
|
||||
return record;
|
||||
}
|
||||
}
|
||||
ScrDyn *method = scr_using_get_method(value, "dispose", 7, &absent);
|
||||
if (!method) {
|
||||
if (absent && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
|
||||
/* A class instance seen through an `unknown` or Disposable-typed
|
||||
* value: its dyn view carries no methods. */
|
||||
static const char message[] =
|
||||
"disposing a class instance through a Disposable- or unknown-typed value has no lowering "
|
||||
"yet (declare the variable with the class type)";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
} else if (absent) {
|
||||
static const char message[] = "Symbol(Symbol.dispose) is not a function";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (!async) return method;
|
||||
ScrDyn *record = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(record, method);
|
||||
scr_dyn_arr_push(record, scr_dyn_new_bool(true));
|
||||
return record;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) {
|
||||
if (method->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
|
||||
bool sync_fallback = false;
|
||||
const ScrDyn *callee = method;
|
||||
if (method->kind == SCR_DYN_ARR) {
|
||||
callee = method->v.arr.items[0];
|
||||
sync_fallback = method->v.arr.items[1]->v.b;
|
||||
}
|
||||
scr_dyn_this_push_dyn(value);
|
||||
ScrDyn *result = scr_dyn_call(callee, NULL, 0, "[Symbol.dispose]");
|
||||
scr_dyn_this_pop();
|
||||
if (!result || !sync_fallback) return result;
|
||||
scr_dyn_release(result);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user