mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 04:14:26 +00:00
Merge pull request #6641 from dotCooCoo/fix/scriptcheck-identifier-chars
Script checker: names follow Unicode ID_Start and ID_Continue
This commit is contained in:
Generated
+1
@@ -6726,6 +6726,7 @@ name = "vectorcraft-scriptcheck"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"unicode-general-category",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The JavaScript parser's lexer (`vectorcraft-js`) and the script checker's (`vectorcraft-scriptcheck`)
|
||||
//! read the same scripts the same way: both crates stand alone, so each has its own lexer, and these
|
||||
//! tests run both over one set of scripts and fail on any disagreement about the lines a script has,
|
||||
//! its preprocessor lines, and where a string, comment or regular expression that is never closed
|
||||
//! starts. The scripts stay inside ECMAScript 3, the language both read (no XML literals, which
|
||||
//! its preprocessor lines, where a string, comment or regular expression that is never closed
|
||||
//! starts, and the characters a name may hold. The scripts stay inside ECMAScript 3, the language both read (no XML literals, which
|
||||
//! only the checker reads).
|
||||
|
||||
use vectorcraft_js::{Options, TokenKind, parse, tokenize};
|
||||
@@ -88,6 +88,42 @@ fn both_lexers_find_the_same_unclosed_token_on_the_same_line() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Characters on both sides of the identifier rule: `Other_ID_Start`, `Other_ID_Continue`, connector
|
||||
/// punctuation, marks, digits and letters of other scripts, and a superscript digit, a
|
||||
/// `Pattern_Syntax` letter and punctuation that no name holds.
|
||||
const NAME_CHARS: &[char] = &[
|
||||
'\u{AA}', '\u{B2}', '\u{B7}', '\u{D7}', '\u{300}', '\u{387}', '\u{660}', '\u{1369}', '\u{1885}', '\u{19DA}', '\u{200C}', '\u{200D}', '\u{203F}',
|
||||
'\u{2040}', '\u{2118}', '\u{212E}', '\u{2160}', '\u{2E2F}', '\u{3005}', '\u{309B}', '\u{30FB}', '\u{FE33}', '\u{FF10}', '\u{FF65}',
|
||||
];
|
||||
|
||||
/// Whether the parser reads `src` as one identifier spelling `name`.
|
||||
fn one_identifier(src: &str, name: &str) -> bool {
|
||||
let want = TokenKind::Ident(name.to_string());
|
||||
tokenize(src, &js_options()).is_ok_and(|tokens| tokens.iter().filter(|t| t.kind != TokenKind::Eof).map(|t| &t.kind).eq([&want]))
|
||||
}
|
||||
|
||||
/// Whether the checker lists the member `app.<src>` as `app.<name>`.
|
||||
fn checker_member(src: &str, name: &str) -> bool {
|
||||
let report = check("script.jsx", format!("app.{src};\n").as_bytes(), None, &Limits::default());
|
||||
report.members.iter().any(|m| m.name == format!("app.{name}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_lexers_take_the_same_characters_into_names() {
|
||||
assert!(checker_member("a\u{2118}b", "a\u{2118}b") && one_identifier("a\u{2118}b", "a\u{2118}b"));
|
||||
for &c in NAME_CHARS {
|
||||
let escape = format!("\\u{:04X}", u32::from(c));
|
||||
for (src, name) in [
|
||||
(format!("a{c}b"), format!("a{c}b")),
|
||||
(format!("{c}b"), format!("{c}b")),
|
||||
(format!("a{escape}b"), format!("a{c}b")),
|
||||
(format!("{escape}b"), format!("{c}b")),
|
||||
] {
|
||||
assert_eq!(checker_member(&src, &name), one_identifier(&src, &name), "{src:?} (U+{:04X})", u32::from(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_parser_reads_a_regexp_wherever_the_checker_does() {
|
||||
// Each `/` that divides has no second `/` after it on its line, so a lexer that read one as a
|
||||
|
||||
@@ -9,5 +9,8 @@ rust-version.workspace = true
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
unicode-general-category = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
|
||||
@@ -116,8 +116,9 @@ pub(crate) fn scan<'s>(src: &'s str, declared: &Declared<'s>, file: Option<&str>
|
||||
}
|
||||
|
||||
/// `src` with the `\uXXXX` escapes in its names written as the characters they stand for (ECMA-262
|
||||
/// 3rd edition, section 7.6), and `src` itself when no name has one. An escape for a character no
|
||||
/// name may hold stays as written. Line breaks don't change, so every line keeps its number.
|
||||
/// 3rd edition, section 7.6), and `src` itself when no name has one. An escape for a character a
|
||||
/// name may not hold where the escape stands stays as written. Line breaks don't change, so every
|
||||
/// line keeps its number.
|
||||
pub(crate) fn unescaped<'a>(src: Cow<'a, str>, acc: &mut Acc) -> Cow<'a, str> {
|
||||
match unescape_names(&src, acc) {
|
||||
Some(out) => Cow::Owned(out),
|
||||
@@ -145,20 +146,17 @@ fn unescape_names(src: &str, acc: &mut Acc) -> Option<String> {
|
||||
changed.then_some(out)
|
||||
}
|
||||
|
||||
/// The name `text` with each `\uXXXX` escape for a character a name may hold written as that
|
||||
/// character.
|
||||
/// The name `text` with each `\uXXXX` escape for a character a name may hold where it stands written
|
||||
/// as that character: a character a name may start with first, and one a name may hold after that.
|
||||
fn unescape_name(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut rest = text;
|
||||
while let Some(i) = rest.find("\\u") {
|
||||
out.push_str(rest.get(..i).unwrap_or_default());
|
||||
let after = rest.get(i + 2..).unwrap_or_default();
|
||||
let c = after
|
||||
.get(..4)
|
||||
.filter(|hex| hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
|
||||
.and_then(char::from_u32)
|
||||
.filter(|&c| crate::lex::ident_part(c));
|
||||
let first = out.is_empty();
|
||||
let c = crate::lex::escaped(rest.get(i..).unwrap_or_default())
|
||||
.filter(|&c| if first { crate::lex::ident_start(c) } else { crate::lex::ident_part(c) });
|
||||
match c {
|
||||
Some(c) => {
|
||||
out.push(c);
|
||||
|
||||
+105
-16
@@ -8,6 +8,10 @@
|
||||
//! level and the levels of XML literals and their code are kept on stacks capped at the nesting
|
||||
//! limit.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use unicode_general_category::{GeneralCategory as G, get_general_category};
|
||||
|
||||
use crate::catalog;
|
||||
use crate::report::{TOO_DEEP, UNTERMINATED_COMMENT, UNTERMINATED_REGEXP, UNTERMINATED_STRING, UNTERMINATED_TEMPLATE};
|
||||
|
||||
@@ -130,13 +134,105 @@ fn is_blank(c: char) -> bool {
|
||||
!is_break(c) && (c.is_whitespace() || c == '\u{feff}')
|
||||
}
|
||||
|
||||
fn ident_start(c: char) -> bool {
|
||||
c == '$' || c == '_' || c.is_ascii_alphabetic() || (!c.is_ascii() && c.is_alphabetic())
|
||||
/// A character a name may start with: a Unicode `ID_Start` character, `$` or `_` (the Names and
|
||||
/// Keywords clause of current ECMA-262 editions). `ID_Start` is the letters (categories Lu, Ll, Lt,
|
||||
/// Lm, Lo and Nl) and `Other_ID_Start`, less `Pattern_Syntax` and `Pattern_White_Space` (Unicode
|
||||
/// Standard Annex #31). The categories come from Unicode 16.0.
|
||||
pub(crate) fn ident_start(c: char) -> bool {
|
||||
match c {
|
||||
'$' | '_' => true,
|
||||
_ if c.is_ascii() => c.is_ascii_alphabetic(),
|
||||
_ if is_other_id_start(c) => true,
|
||||
_ => {
|
||||
matches!(
|
||||
get_general_category(c),
|
||||
G::UppercaseLetter | G::LowercaseLetter | G::TitlecaseLetter | G::ModifierLetter | G::OtherLetter | G::LetterNumber
|
||||
) && !is_pattern_syntax(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A character a name may hold after its first.
|
||||
/// A character a name may hold after its first: a Unicode `ID_Continue` character or `$`.
|
||||
/// `ID_Continue` adds to `ID_Start` the decimal digits (Nd), combining marks (Mn, Mc), connector
|
||||
/// punctuation (Pc, which holds `_`) and `Other_ID_Continue` (which holds the zero width non-joiner
|
||||
/// and joiner), less `Pattern_Syntax` and `Pattern_White_Space`.
|
||||
pub(crate) fn ident_part(c: char) -> bool {
|
||||
ident_start(c) || c.is_ascii_digit() || (!c.is_ascii() && c.is_alphanumeric())
|
||||
match c {
|
||||
'0'..='9' => true,
|
||||
_ if ident_start(c) => true,
|
||||
_ if c.is_ascii() => false,
|
||||
_ if is_other_id_continue(c) => true,
|
||||
_ => {
|
||||
matches!(get_general_category(c), G::NonspacingMark | G::SpacingMark | G::DecimalNumber | G::ConnectorPunctuation)
|
||||
&& !is_pattern_syntax(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The character the `\uXXXX` escape at the start of `s` stands for: `None` when `s` doesn't start
|
||||
/// with `\u` and four hexadecimal digits, or when they give a lone surrogate.
|
||||
pub(crate) fn escaped(s: &str) -> Option<char> {
|
||||
s.strip_prefix("\\u")?
|
||||
.get(..4)
|
||||
.filter(|hex| hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
|
||||
.and_then(char::from_u32)
|
||||
}
|
||||
|
||||
/// `Other_ID_Start` (Unicode PropList 16.0): characters kept in `ID_Start` though their category is
|
||||
/// not a letter.
|
||||
fn is_other_id_start(c: char) -> bool {
|
||||
matches!(c, '\u{1885}' | '\u{1886}' | '\u{2118}' | '\u{212E}' | '\u{309B}' | '\u{309C}')
|
||||
}
|
||||
|
||||
/// `Other_ID_Continue` (Unicode PropList 16.0): characters kept in `ID_Continue` though their
|
||||
/// category is not a letter, mark, digit or connector.
|
||||
fn is_other_id_continue(c: char) -> bool {
|
||||
matches!(c, '\u{B7}' | '\u{387}' | '\u{1369}'..='\u{1371}' | '\u{19DA}' | '\u{200C}' | '\u{200D}' | '\u{30FB}' | '\u{FF65}')
|
||||
}
|
||||
|
||||
/// Non-ASCII `Pattern_Syntax` ranges (Unicode PropList). Unicode does not change this set, and no
|
||||
/// identifier character belongs to it. `Pattern_White_Space` outside ASCII (U+0085, U+200E, U+200F,
|
||||
/// U+2028 and U+2029) holds no letter, mark, digit or connector, so the category checks leave it out.
|
||||
const PATTERN_SYNTAX: [(char, char); 23] = [
|
||||
('\u{A1}', '\u{A7}'),
|
||||
('\u{A9}', '\u{A9}'),
|
||||
('\u{AB}', '\u{AC}'),
|
||||
('\u{AE}', '\u{AE}'),
|
||||
('\u{B0}', '\u{B1}'),
|
||||
('\u{B6}', '\u{B6}'),
|
||||
('\u{BB}', '\u{BB}'),
|
||||
('\u{BF}', '\u{BF}'),
|
||||
('\u{D7}', '\u{D7}'),
|
||||
('\u{F7}', '\u{F7}'),
|
||||
('\u{2010}', '\u{2027}'),
|
||||
('\u{2030}', '\u{203E}'),
|
||||
('\u{2041}', '\u{2053}'),
|
||||
('\u{2055}', '\u{205E}'),
|
||||
('\u{2190}', '\u{245F}'),
|
||||
('\u{2500}', '\u{2775}'),
|
||||
('\u{2794}', '\u{2BFF}'),
|
||||
('\u{2E00}', '\u{2E7F}'),
|
||||
('\u{3001}', '\u{3003}'),
|
||||
('\u{3008}', '\u{3020}'),
|
||||
('\u{3030}', '\u{3030}'),
|
||||
('\u{FD3E}', '\u{FD3F}'),
|
||||
('\u{FE45}', '\u{FE46}'),
|
||||
];
|
||||
|
||||
/// Whether a non-ASCII character is `Pattern_Syntax`.
|
||||
fn is_pattern_syntax(c: char) -> bool {
|
||||
PATTERN_SYNTAX
|
||||
.binary_search_by(|&(lo, hi)| {
|
||||
if hi < c {
|
||||
Ordering::Less
|
||||
} else if lo > c {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// After `<`, the start of an XML literal: a name, `>` (a list), `!` (a comment or CDATA), `?` (a
|
||||
@@ -300,20 +396,13 @@ impl<'s> Lexer<'s> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The rest of a name (its first character included): letters, digits, `$`, `_` and `\u`
|
||||
/// escapes.
|
||||
/// The rest of a name (its first character included): the characters [`ident_part`] allows,
|
||||
/// written out or as `\uXXXX` escapes. An escape for any other character, or one without four
|
||||
/// hexadecimal digits, ends the name.
|
||||
fn name(&mut self) {
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some('\\') if self.peek_at(1) == Some('u') => {
|
||||
self.skip(2);
|
||||
for _ in 0..4 {
|
||||
match self.peek() {
|
||||
Some(h) if h.is_ascii_hexdigit() => self.bump(h),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('\\') if escaped(self.rest()).is_some_and(ident_part) => self.skip(6),
|
||||
Some(c) if ident_part(c) => self.bump(c),
|
||||
_ => break,
|
||||
}
|
||||
@@ -820,7 +909,7 @@ impl<'s> Iterator for Lexer<'s> {
|
||||
self.close_level();
|
||||
self.bump('}');
|
||||
return Some(self.markup_token(start, line, T::XmlMore, stmt, expr));
|
||||
} else if ident_start(c) || (c == '\\' && self.peek_at(1) == Some('u')) {
|
||||
} else if ident_start(c) || (c == '\\' && escaped(self.rest()).is_some_and(ident_start)) {
|
||||
self.name();
|
||||
T::Ident
|
||||
} else if c.is_ascii_digit() || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit())) {
|
||||
|
||||
@@ -268,6 +268,99 @@ fn names_written_with_escapes_are_read_as_the_names_they_spell() {
|
||||
assert_eq!(r.lines, 3);
|
||||
}
|
||||
|
||||
/// `src` with the escapes in its names written as the characters they stand for.
|
||||
fn unescaped(src: &str) -> String {
|
||||
let limits = Limits::default();
|
||||
crate::analyze::unescaped(std::borrow::Cow::Borrowed(src), &mut crate::report::Acc::new(&limits)).into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_are_made_of_the_unicode_identifier_characters() {
|
||||
// U+2118 (Other_ID_Start) starts a name, and U+203F (connector punctuation), U+00B7
|
||||
// (Other_ID_Continue) and the zero width non-joiner and joiner continue one.
|
||||
for name in ["\u{2118}a", "a\u{203F}b", "a\u{B7}b", "a\u{200C}b", "a\u{200D}b", "_\u{2118}", "$_a1", "\u{E9}t\u{E9}"] {
|
||||
assert_eq!(tokens(name), [(T::Ident, name)], "{name:?}");
|
||||
}
|
||||
for (src, want) in [
|
||||
// Characters that only continue a name, and digits, don't start one.
|
||||
("\u{203F}a", [(T::Other, "\u{203F}"), (T::Ident, "a")]),
|
||||
("\u{B7}a", [(T::Other, "\u{B7}"), (T::Ident, "a")]),
|
||||
("\u{200C}a", [(T::Other, "\u{200C}"), (T::Ident, "a")]),
|
||||
("0a", [(T::Number, "0"), (T::Ident, "a")]),
|
||||
// A superscript digit (category No) and a letter that is Pattern_Syntax are in no name.
|
||||
("a\u{B2}", [(T::Ident, "a"), (T::Other, "\u{B2}")]),
|
||||
("a\u{2E2F}", [(T::Ident, "a"), (T::Other, "\u{2E2F}")]),
|
||||
("\u{2E2F}a", [(T::Other, "\u{2E2F}"), (T::Ident, "a")]),
|
||||
// `#` and a name away from the start of a line, and a regular expression's flags.
|
||||
("a #\u{2118}b", [(T::Ident, "a"), (T::Other, "#\u{2118}b")]),
|
||||
("=/a/g\u{B2}", [(T::RegExp, "/a/g"), (T::Other, "\u{B2}")]),
|
||||
] {
|
||||
let mut got = tokens(src);
|
||||
if src.starts_with('=') {
|
||||
got.remove(0);
|
||||
}
|
||||
assert_eq!(got, want, "{src:?}");
|
||||
}
|
||||
assert_eq!(tokens("a #\u{B2}"), [(T::Ident, "a"), (T::Other, "#"), (T::Other, "\u{B2}")]);
|
||||
assert_eq!(tokens("=/a/g\u{200D}"), [(T::Punct, "="), (T::RegExp, "/a/g\u{200D}")]);
|
||||
// The code in an XML literal's braces reads names by the same rule.
|
||||
assert!(tokens("var x = <a>{\u{2118}b + c\u{203F}d}</a>;").iter().filter(|t| t.0 == T::Ident).any(|t| t.1 == "c\u{203F}d"));
|
||||
assert!(tokens("var x = <a>{\u{2118}b}</a>;").contains(&(T::Ident, "\u{2118}b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_escape_stands_for_a_character_only_where_a_name_may_hold_it() {
|
||||
for (src, want) in [
|
||||
("\\u2118a", "\u{2118}a"),
|
||||
("a\\u203Fb", "a\u{203F}b"),
|
||||
("a\\u00B7b", "a\u{B7}b"),
|
||||
("a\\u200Cb", "a\u{200C}b"),
|
||||
("a\\u200Db", "a\u{200D}b"),
|
||||
("a\\u0030", "a0"),
|
||||
("\\u0024\\u005F", "$_"),
|
||||
// Not at the start of a name.
|
||||
("\\u0030abc", "\\u0030abc"),
|
||||
("\\u203Fa", "\\u203Fa"),
|
||||
("\\u00B7a", "\\u00B7a"),
|
||||
// In no name.
|
||||
("a\\u00B2", "a\\u00B2"),
|
||||
("a\\u2E2F", "a\\u2E2F"),
|
||||
("\\u2E2F", "\\u2E2F"),
|
||||
// Malformed escapes and a lone surrogate.
|
||||
("\\u", "\\u"),
|
||||
("a\\u12;", "a\\u12;"),
|
||||
("a\\uZZZZ", "a\\uZZZZ"),
|
||||
("a\\uD800", "a\\uD800"),
|
||||
("", ""),
|
||||
] {
|
||||
assert_eq!(unescaped(src), want, "{src:?}");
|
||||
}
|
||||
let r = report("\\u0061pp.a\\u203Fb;\napp.\\u2118c;\n");
|
||||
let members: Vec<&str> = r.members.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(members, ["app.a\u{203F}b", "app.\u{2118}c"]);
|
||||
// An escape a name may not hold where it stands, or a malformed one, is not part of a name.
|
||||
for (src, want) in [
|
||||
("\\u0030a", [(T::Other, "\\"), (T::Ident, "u0030a")]),
|
||||
("a\\u00B2", [(T::Ident, "a"), (T::Other, "\\")]),
|
||||
("a\\u12", [(T::Ident, "a"), (T::Other, "\\")]),
|
||||
("a\\uD800", [(T::Ident, "a"), (T::Other, "\\")]),
|
||||
] {
|
||||
assert_eq!(tokens(src).get(..2), Some(&want[..]), "{src:?}");
|
||||
}
|
||||
assert_eq!(tokens("a\\u0030\\u200C"), [(T::Ident, "a\\u0030\\u200C")]);
|
||||
let r = report("app.\\u0030abc;\napp.a\\u00B2;\n");
|
||||
assert!(r.members.iter().all(|m| !m.name.contains('\\') && !m.name.contains("0abc")), "{:?}", r.members);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_escapes_in_bulk_are_read_once_through() {
|
||||
let src = format!("{}\n{}\nalert(1);\n", "\\u\\uD800\\u12\\u0030\\u2118".repeat(20_000), "a\u{203F}".repeat(20_000));
|
||||
let r = report(&src);
|
||||
assert_eq!(r.lines, 3);
|
||||
assert_eq!(feature(&r, "notify").map(|f| f.lines.as_slice()), Some(&[3][..]));
|
||||
assert!(r.steps <= 5 * src.len() + 16, "{} steps for {} bytes", r.steps, src.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scripts_own_platform_names_are_not_the_platforms() {
|
||||
let r = report(concat!(
|
||||
|
||||
@@ -747,6 +747,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (the same reply bytes from one lookup instead of summarizing the whole document: a leaf read on a 2,000-object document drops from ~20 ms to ~0.06 ms), large containers slice with `document.node {depth?, childLimit?}` (truncated levels report `childCount`), `document.inspect` and the `inspect_document` tool slice the same way so a depth-0 read is the layer skeleton (~9.7 KB instead of ~2 MB on a 2,000-object document), `document.find` locates nodes by name/kind/text with ancestor paths, and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
|
||||
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785); `run --commands FILE` (or `-` for standard input) reads the steps from a JSON array or JSON Lines, past the Windows command-line limit (#3147, `apps/vectorcraft-cli/tests/it/cli.rs` `run_reads_*`, `run_refuses_malformed_commands_naming_the_step`).
|
||||
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
|
||||
- Check Script reads names by the same identifier rule as `vectorcraft-js`: Unicode 16.0 `ID_Start` characters, `$` and `_` start a name, and `ID_Continue` characters and `$` continue it (https://www.unicode.org/reports/tr31/, https://tc39.es/ecma262/#sec-names-and-keywords). U+2118, U+203F, U+00B7 and the zero width non-joiner and joiner are name characters, and U+00B2 and U+2E2F are not. A `\uXXXX` escape is read as its character only where a name may hold it, so `0abc` does not become a name that starts with a digit. Tests: `crates/scriptcheck/src/tests.rs` (`names_are_made_of_the_unicode_identifier_characters`, `an_escape_stands_for_a_character_only_where_a_name_may_hold_it`), `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs` (`both_lexers_take_the_same_characters_into_names`).
|
||||
- `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). This entry covers parsing; the next entry covers running scripts. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`.
|
||||
- `vectorcraft-js` identifiers, written out or as `\uXXXX` and `\u{...}` escapes, take the characters Unicode's `ID_Start` and `ID_Continue` properties allow (https://www.unicode.org/reports/tr31/), plus `$` and, at the start, `_`, as later ECMAScript editions define them (https://tc39.es/ecma262/#sec-names-and-keywords). `Other_ID_Start` characters such as U+2118 and `Other_ID_Continue` characters such as U+00B7 and the zero width non-joiner and joiner are accepted, and no `Pattern_Syntax` or `Pattern_White_Space` character is. Letter categories come from Unicode 16.0, so characters assigned in later Unicode versions are not yet identifier characters. Tests: `crates/js/src/tests.rs` (`identifier_characters_are_unicode_16_id_start_and_id_continue`, which checks every code point, `identifiers_take_the_other_id_start_and_continue_characters`, `identifiers_refuse_pattern_syntax_surrogates_and_bad_escapes`, `no_pattern_syntax_or_white_space_character_is_an_identifier_character`).
|
||||
- `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap, with the built-ins the next two entries list. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only the built-ins present: 4,280 of 4,286 runs pass (4 of the others expect the completion values of later editions, and 2 expect a configurable function `length`). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
|
||||
Reference in New Issue
Block a user