Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions api-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#undef NDEBUG
#endif
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "quickjs.h"
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all that is holy, please make this a function.


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[] =
Expand Down Expand Up @@ -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();
Expand Down
51 changes: 35 additions & 16 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 */
Comment on lines +38101 to +38102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please drop the "add 1 to i" comment.

s->col_num = 1;
if (col > 0 && input_len < (size_t)(INT32_MAX - col))
s->col_num = col;
Comment on lines +38104 to +38105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s->col_num = max_int(1, col);

I don't like how the current code conflates the offset (human visible) with the position in the buffer (machine visible), please do something about that.

(In a previous age I would've made concrete suggestions but since it's going into a slop bot that feels like wasted effort.)

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,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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, "<input>", 1, flags, scope_idx);
ret = JS_EvalInternal(ctx, this_obj, str, len, "<input>", 1, 1, flags,
scope_idx);
JS_FreeCString(ctx, str);
return ret;

Expand All @@ -38317,22 +38332,25 @@ JSValue JS_EvalThis2(JSContext *ctx, JSValueConst this_obj,
{
const char *filename = "<unnamed>";
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;

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

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, "<unnamed>", 1,
val = __JS_EvalInternal(ctx, JS_UNDEFINED, input, input_len, "<unnamed>",
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);
Expand Down
4 changes: 3 additions & 1 deletion quickjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading