diff --git a/api-test.c b/api-test.c index ef38a7f98..21aa60930 100644 --- a/api-test.c +++ b/api-test.c @@ -2,6 +2,7 @@ #undef NDEBUG #endif #include +#include #include #include #include "quickjs.h" @@ -928,6 +929,164 @@ static void new_errors(void) JS_FreeRuntime(rt); } +// JSEvalOptions.col_num places the first line of the snippet at a column of +// the enclosing document, the way JSEvalOptions.line_num places it on a line. +// Only the first line is shifted: every subsequent line starts at column 1. +static void eval_options_col_num(void) +{ + // returns "line:col" of the first frame of the exception raised by `code` + // evaluated under `options`, or NULL if it did not throw + char buf[64], buf2[64]; + JSValue ret, exc, stack; + const char *s, *p; + JSEvalOptions options; + +#define EVAL_LOC(code, opts) \ + (buf[0] = '\0', \ + ret = JS_Eval2(ctx, code, strlen(code), opts), \ + assert(JS_IsException(ret)), \ + JS_FreeValue(ctx, ret), \ + exc = JS_GetException(ctx), \ + stack = JS_GetPropertyStr(ctx, exc, "stack"), \ + s = JS_ToCString(ctx, stack), \ + assert(s), \ + p = strstr(s, "eval.js:"), \ + assert(p), \ + snprintf(buf, sizeof(buf), "%.*s", \ + (int)strcspn(p + 8, "\n )"), p + 8), \ + JS_FreeCString(ctx, s), \ + JS_FreeValue(ctx, stack), \ + JS_FreeValue(ctx, exc), \ + buf) + + JSRuntime *rt = new_runtime(); + JSContext *ctx = JS_NewContext(rt); + + options = (JSEvalOptions){ + .version = JS_EVAL_OPTIONS_VERSION, + .filename = "eval.js", + }; + + // the baseline: three spaces then an undefined global, so column 4 + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + + // an unset col_num is column 1, and so is an explicit 1 + options.col_num = 0; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + options.col_num = 1; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + + // starting at column 10 moves the first line right by 9 + options.col_num = 10; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:13")); + assert(!strcmp(EVAL_LOC("nope", &options), "1:10")); + + // a syntax error is placed the same way + assert(!strcmp(EVAL_LOC("let x = ;", &options), "1:18")); + + // a large offset is carried through intact + options.col_num = 100000; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:100003")); + options.col_num = 10; + + // only the first line is shifted + assert(!strcmp(EVAL_LOC("1;\n nope", &options), "2:4")); + assert(!strcmp(EVAL_LOC("1;\nlet y = ;", &options), "2:9")); + + // col_num composes with line_num + options.line_num = 5; + assert(!strcmp(EVAL_LOC(" nope", &options), "5:13")); + assert(!strcmp(EVAL_LOC("1;\n nope", &options), "6:4")); + options.line_num = 0; + + // a version 1 caller has no col_num field at all, so a stale value in + // that position must be ignored rather than read + options.version = 1; + options.col_num = 10; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + options.version = JS_EVAL_OPTIONS_VERSION; + + // versions outside the supported range are refused + { + static const int bad[] = { 0, -1, JS_EVAL_OPTIONS_VERSION + 1 }; + size_t i; + for (i = 0; i < countof(bad); i++) { + options.version = bad[i]; + ret = JS_Eval2(ctx, "1", 1, &options); + assert(JS_IsException(ret)); + JS_FreeValue(ctx, ret); + exc = JS_GetException(ctx); + s = JS_ToCString(ctx, exc); + assert(s); + assert(!strcmp(s, "InternalError: bad JSEvalOptions version")); + JS_FreeCString(ctx, s); + JS_FreeValue(ctx, exc); + } + options.version = JS_EVAL_OPTIONS_VERSION; + } + + // a successful eval is unaffected by the offset + options.col_num = 10; + ret = JS_Eval2(ctx, "1 + 1", 5, &options); + assert(!JS_IsException(ret)); + assert(JS_VALUE_GET_INT(ret) == 2); + JS_FreeValue(ctx, ret); + + // a negative offset is not an offset at all + options.col_num = -1; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + options.col_num = -100000; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + + // neither is one so large that the columns of the source could not be + // numbered from it without overflowing + options.col_num = INT_MAX; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + options.col_num = INT_MAX - 1; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + options.col_num = INT_MAX - 7; + assert(!strcmp(EVAL_LOC(" nope", &options), "1:4")); + + // the largest offset that does still fit is used as given: " nope" is + // seven bytes, so the last column the parser can reach is col + 7 + options.col_num = INT_MAX - 8; + snprintf(buf2, sizeof(buf2), "1:%d", INT_MAX - 5); + assert(!strcmp(EVAL_LOC(" nope", &options), buf2)); + + // every frame of a multi-frame stack is numbered from the same origin + options.col_num = 10; + options.line_num = 1; + assert(!strcmp(EVAL_LOC("function f() { nope }\n f()", &options), "1:25")); + assert(!strcmp(EVAL_LOC("(function () { nope })()", &options), "1:25")); + + // a module is offset the same way a script is + options.eval_flags = JS_EVAL_TYPE_MODULE; + assert(!strcmp(EVAL_LOC(" let x = ;", &options), "1:20")); + assert(!strcmp(EVAL_LOC("1;\nlet x = ;", &options), "2:9")); + options.eval_flags = 0; + + // a source whose first line is empty is back to column 1 immediately + assert(!strcmp(EVAL_LOC("\n nope", &options), "2:2")); + + // JSON parsing shares the tokenizer but has no column origin of its own + ret = JS_ParseJSON(ctx, "{\"a\":1}", 7, "j.json"); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); + ret = JS_ParseJSON(ctx, "{\"a\":}", 6, "j.json"); + assert(JS_IsException(ret)); + JS_FreeValue(ctx, ret); + exc = JS_GetException(ctx); + s = JS_ToCString(ctx, exc); + assert(s); + JS_FreeCString(ctx, s); + JS_FreeValue(ctx, exc); + +#undef EVAL_LOC + + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + static void backtrace_oom_callsite_array(void) { static const char setup_code[] = @@ -1840,6 +1999,7 @@ int main(void) promise_hook(); dump_memory_usage(); new_errors(); + eval_options_col_num(); backtrace_oom_current_exception(); backtrace_oom_callsite_array(); proxy_own_keys_huge_length(); diff --git a/quickjs.c b/quickjs.c index bbac00c33..d33d9b8d1 100644 --- a/quickjs.c +++ b/quickjs.c @@ -615,7 +615,8 @@ struct JSContext { /* if NULL, eval is not supported */ JSValue (*eval_internal)(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, - const char *filename, int line, int flags, int scope_idx); + const char *filename, int line, int col, int flags, + int scope_idx); void *user_opaque; }; @@ -1469,7 +1470,8 @@ static void js_async_function_resolve_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, - const char *filename, int line, int flags, int scope_idx); + const char *filename, int line, int col, + int flags, int scope_idx); static void js_free_module_def(JSContext *ctx, JSModuleDef *m); static int js_module_attributes_equal(JSContext *ctx, JSValueConst attr1, JSValueConst attr2); @@ -38088,21 +38090,31 @@ static __exception int js_parse_program(JSParseState *s) static void js_parse_init(JSContext *ctx, JSParseState *s, const char *input, size_t input_len, - const char *filename, int line) + const char *filename, int line, int col) { + int col_off; + memset(s, 0, sizeof(*s)); s->ctx = ctx; s->filename = filename; s->line_num = line; + /* number the first line from `col`, but only while every column of the + source still fits an int; anything else is numbered from 1 */ s->col_num = 1; + if (col > 0 && input_len < (size_t)(INT32_MAX - col)) + s->col_num = col; s->buf_start = s->buf_ptr = (const uint8_t *)input; s->buf_end = s->buf_ptr + input_len; - s->line_start = s->buf_ptr; + /* the first line starts at column `col` of the enclosing document, so + back the two column origins up by that much; both are reset at the + first line terminator, so only the first line is affected */ + col_off = s->col_num - 1; + s->line_start = s->buf_ptr - col_off; s->mark = s->buf_ptr + min_int(1, input_len); - s->eol = s->buf_ptr; + s->eol = s->buf_ptr - col_off; s->token.val = ' '; - s->token.line_num = 1; - s->token.col_num = 1; + s->token.line_num = line; + s->token.col_num = s->col_num; } static JSValue JS_EvalFunctionInternal(JSContext *ctx, JSValue fun_obj, @@ -38148,7 +38160,8 @@ JSValue JS_EvalFunction(JSContext *ctx, JSValue fun_obj) /* `export_name` and `input` may be pure ASCII or UTF-8 encoded */ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, - const char *filename, int line, int flags, int scope_idx) + const char *filename, int line, int col, + int flags, int scope_idx) { JSParseState s1, *s = &s1; int err, eval_type; @@ -38160,7 +38173,7 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, JSModuleDef *m; bool is_strict_mode; - js_parse_init(ctx, s, input, input_len, filename, line); + js_parse_init(ctx, s, input, input_len, filename, line, col); skip_shebang(&s->buf_ptr, s->buf_end); eval_type = flags & JS_EVAL_TYPE_MASK; @@ -38265,7 +38278,8 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, /* the indirection is needed to make 'eval' optional */ static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, - const char *filename, int line, int flags, int scope_idx) + const char *filename, int line, int col, + int flags, int scope_idx) { JSRuntime *rt = ctx->rt; @@ -38277,7 +38291,7 @@ static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, ctx->error_back_trace = JS_UNDEFINED; } return ctx->eval_internal(ctx, this_obj, input, input_len, filename, line, - flags, scope_idx); + col, flags, scope_idx); } static JSValue JS_EvalObject(JSContext *ctx, JSValueConst this_obj, @@ -38292,7 +38306,8 @@ static JSValue JS_EvalObject(JSContext *ctx, JSValueConst this_obj, str = JS_ToCStringLen(ctx, &len, val); if (!str) return JS_EXCEPTION; - ret = JS_EvalInternal(ctx, this_obj, str, len, "", 1, flags, scope_idx); + ret = JS_EvalInternal(ctx, this_obj, str, len, "", 1, 1, flags, + scope_idx); JS_FreeCString(ctx, str); return ret; @@ -38317,14 +38332,17 @@ JSValue JS_EvalThis2(JSContext *ctx, JSValueConst this_obj, { const char *filename = ""; int line = 1; + int col = 1; int eval_flags = 0; if (options) { - if (options->version != JS_EVAL_OPTIONS_VERSION) + if (options->version < 1 || options->version > JS_EVAL_OPTIONS_VERSION) return JS_ThrowInternalError(ctx, "bad JSEvalOptions version"); if (options->filename) filename = options->filename; if (options->line_num != 0) line = options->line_num; + if (options->version >= 2 && options->col_num != 0) + col = options->col_num; eval_flags = options->eval_flags; } JSValue ret; @@ -38332,7 +38350,7 @@ JSValue JS_EvalThis2(JSContext *ctx, JSValueConst this_obj, assert((eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_GLOBAL || (eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE); ret = JS_EvalInternal(ctx, this_obj, input, input_len, filename, line, - eval_flags, -1); + col, eval_flags, -1); return ret; } @@ -51178,7 +51196,7 @@ static JSValue JS_ParseJSON_internal(JSContext *ctx, const char *buf, size_t buf JSParseState s1, *s = &s1; JSValue val = JS_UNDEFINED; - js_parse_init(ctx, s, buf, buf_len, filename, 1); + js_parse_init(ctx, s, buf, buf_len, filename, 1, 1); if (json_next_token(s)) goto fail; val = json_parse_value(s, pr); @@ -64776,7 +64794,8 @@ bool JS_DetectModule(const char *input, size_t input_len) return false; } JS_AddIntrinsicRegExpCompiler(ctx); // otherwise regexp literals don't parse - val = __JS_EvalInternal(ctx, JS_UNDEFINED, input, input_len, "", 1, + val = __JS_EvalInternal(ctx, JS_UNDEFINED, input, input_len, "", + 1, 1, JS_EVAL_TYPE_MODULE|JS_EVAL_FLAG_COMPILE_ONLY, -1); if (JS_IsException(val)) { const char *msg = JS_ToCString(ctx, rt->current_exception); diff --git a/quickjs.h b/quickjs.h index 0617a61ab..4575bdf5d 100644 --- a/quickjs.h +++ b/quickjs.h @@ -694,13 +694,15 @@ typedef struct JSClassDef { JSClassExoticMethods *exotic; } JSClassDef; -#define JS_EVAL_OPTIONS_VERSION 1 +#define JS_EVAL_OPTIONS_VERSION 2 typedef struct JSEvalOptions { int version; int eval_flags; const char *filename; int line_num; + // added in version 2 + int col_num; // can add new fields in ABI-compatible manner by incrementing JS_EVAL_OPTIONS_VERSION } JSEvalOptions;