diff --git a/javascript/selenium-webdriver/BUILD.bazel b/javascript/selenium-webdriver/BUILD.bazel index 1c61e29ca73a5..fc129df4342d5 100644 --- a/javascript/selenium-webdriver/BUILD.bazel +++ b/javascript/selenium-webdriver/BUILD.bazel @@ -84,8 +84,8 @@ mocha_test( ) # Generate WebDriver BiDi TypeScript modules from CDDL specification. -# extra_cddl_files are merged with the primary BiDi spec before generation so that -# adjacent specs (Permissions, Prefetch, UA Client Hints, Web Bluetooth) are included. +# extra_cddl_files are parsed alongside the primary BiDi spec so that adjacent specs +# (Permissions, Prefetch, UA Client Hints, Web Bluetooth) are included. generate_bidi_library( name = "create-bidi-src", cddl_file = "@webdriver_bidi_all_cddl//file:spec.cddl", diff --git a/javascript/selenium-webdriver/generate_bidi.mjs b/javascript/selenium-webdriver/generate_bidi.mjs index ca990d9c73182..e928eff770a0a 100644 --- a/javascript/selenium-webdriver/generate_bidi.mjs +++ b/javascript/selenium-webdriver/generate_bidi.mjs @@ -145,7 +145,7 @@ function resolveInputPath(p) { async function main() { const { values: args } = parseArgs({ options: { - cddl: { type: 'string' }, + cddl: { type: 'string', multiple: true }, ast: { type: 'string' }, model: { type: 'string' }, 'dump-ast': { type: 'string' }, @@ -157,8 +157,12 @@ async function main() { }) // One pipeline stage per invocation; the flags select the stage. - if (args['dump-ast'] && args.cddl) { - writeJson(args['dump-ast'], parseCddl(args.cddl), 'ast') + if (args['dump-ast'] && args.cddl?.length) { + // The base spec is several CDDL files (webdriver-bidi + the adjacent specs); each + // is parsed independently and their definitions concatenated. Top-level CDDL + // productions are position-independent (refs resolve by name later), so this equals + // parsing one merged file — without a separate merge step or tool. + writeJson(args['dump-ast'], args.cddl.flatMap(parseCddl), 'ast') } else if (args['dump-model'] && args.ast) { writeJson(args['dump-model'], buildModel(readJson(args.ast, 'AST')), 'model', true) } else if (args['output-dir'] && args.ast && args.model) { @@ -166,7 +170,7 @@ async function main() { } else { console.error( 'Usage (one stage per invocation):\n' + - ' generate_bidi.mjs --cddl --dump-ast \n' + + ' generate_bidi.mjs --cddl [--cddl ...] --dump-ast \n' + ' generate_bidi.mjs --ast --dump-model \n' + ' generate_bidi.mjs --ast --model --output-dir [--enhancements ] [--spec-version ]', ) diff --git a/javascript/selenium-webdriver/private/generate_bidi.bzl b/javascript/selenium-webdriver/private/generate_bidi.bzl index d98a1907ba114..4b22be9bbdda8 100644 --- a/javascript/selenium-webdriver/private/generate_bidi.bzl +++ b/javascript/selenium-webdriver/private/generate_bidi.bzl @@ -30,36 +30,6 @@ _DOMAIN_TS_FILES = [ "webextension.ts", ] -def _merge_cddl_impl(ctx): - """Merges one or more CDDL files into a single output file.""" - out = ctx.outputs.out - args = ctx.actions.args() - args.add(out) - args.add_all(ctx.files.srcs) - ctx.actions.run( - inputs = ctx.files.srcs, - outputs = [out], - executable = ctx.executable.tool, - arguments = [args], - mnemonic = "MergeCddl", - progress_message = "Merging CDDL files into %s" % out.short_path, - ) - return [DefaultInfo(files = depset([out]))] - -_merge_cddl = rule( - implementation = _merge_cddl_impl, - attrs = { - "srcs": attr.label_list(allow_files = True, mandatory = True), - "out": attr.output(mandatory = True), - "tool": attr.label( - executable = True, - cfg = "exec", - mandatory = True, - ), - }, - doc = "Merges CDDL specification files into a single file using an external merge tool.", -) - def _compile_bidi_ts_impl(ctx): ts_files = ctx.files.srcs output_subdir = ctx.attr.output_subdir @@ -127,15 +97,14 @@ def generate_bidi_library( generator = None, schema_generator = None, anchors_extractor = None, - merge_tool = "//py/private:merge_cddl", spec_version = "1.0", output_path = "bidi/generated"): - """Macro that merges CDDL, generates BiDi TypeScript modules, and compiles them to JS. + """Macro that generates BiDi TypeScript modules from CDDL and compiles them to JS. Args: name: Base name for the targets. cddl_file: Primary CDDL spec label (webdriver-bidi-all.cddl). - extra_cddl_files: Additional CDDL files merged before generation. + extra_cddl_files: Additional CDDL specs parsed alongside the primary one. dfns_files: webref definition-index files (one per merged spec). When given, the schema step joins them by type name to attach a `specHref` spec link to each type. Optional — omitting them yields a schema with no links. @@ -146,7 +115,6 @@ def generate_bidi_library( generator: The generate_bidi.mjs js_binary label. Defaults to :generate_bidi_script. schema_generator: The project_bidi_schema.mjs js_binary label. Defaults to :project_bidi_schema_script. anchors_extractor: The extract_bidi_anchors.mjs js_binary label. Defaults to :extract_bidi_anchors_script. - merge_tool: Python binary that concatenates CDDL files (output first, then inputs). spec_version: Spec version string passed to the generator. output_path: Output path for generated files within the package (default: bidi/generated). """ @@ -160,32 +128,25 @@ def generate_bidi_library( pkg = native.package_name() ts_src_path = output_path + "_src" - # Step 1: merge CDDL files into one. - # merge_cddl signature: [ ...] - # Uses ctx.actions.run so arguments are passed as an argv list rather than - # a shell command string, avoiding quoting/escaping issues with special chars. - merged_name = name + "_merged_cddl" - _merge_cddl( - name = merged_name, - srcs = [cddl_file] + extra_cddl_files, - out = name + "_merged.cddl", - tool = merge_tool, - ) - - # Step 2: parse the merged CDDL once into the reusable AST artifact. Internal - # input to the schema and the JS generator; not consumed by other bindings. + # Step 1: parse the base specs into the reusable AST artifact. generate_bidi.mjs + # parses each `--cddl` file and concatenates their definitions (no separate merge + # tool). Internal input to the schema and the JS generator; not consumed by other + # bindings. js_run_binary copies its srcs to bin and rejects external/cross-package + # files, so stage each spec into the package first (as the dfns/spec_html steps do). + staged_specs = [] + cddl_args = [] + for i, spec in enumerate([cddl_file] + extra_cddl_files): + staged = name + "_cddl_%d.cddl" % i + copy_file(name = name + "_cddl_copy_%d" % i, src = spec, out = staged) + staged_specs.append(":" + staged) + cddl_args += ["--cddl", "$(location :" + staged + ")"] ast_target = name + "_ast" ast_out = name + "_ast.json" js_run_binary( name = ast_target, - srcs = [":" + merged_name], + srcs = staged_specs, outs = [ast_out], - args = [ - "--cddl", - "$(location :" + merged_name + ")", - "--dump-ast", - pkg + "/" + ast_out, - ], + args = cddl_args + ["--dump-ast", pkg + "/" + ast_out], tool = generator, ) diff --git a/javascript/selenium-webdriver/project_bidi_schema.mjs b/javascript/selenium-webdriver/project_bidi_schema.mjs index 72e10a93ac7b5..a2fad583326de 100644 --- a/javascript/selenium-webdriver/project_bidi_schema.mjs +++ b/javascript/selenium-webdriver/project_bidi_schema.mjs @@ -104,6 +104,10 @@ const typeList = (t) => (Array.isArray(t) ? t : t === undefined || t === null ? const isLiteral = (e) => e && typeof e === 'object' && e.Type === 'literal' const isRef = (e) => e && typeof e === 'object' && e.Type === 'group' && typeof e.Value === 'string' +// An occurrence with no upper bound (`*` / `+`). The parser emits Infinity; the AST's +// JSON round-trip renders that as null, so treat both as unbounded. +const isUnbounded = (occ) => !!occ && (occ.m === null || occ.m === Infinity) + // A `null` keyword or a `nil` prelude ref in a union means the value may be null. const isNullAlt = (e) => e === 'null' || (e && typeof e === 'object' && e.Type === 'group' && PRELUDE[e.Value] === 'null') @@ -242,20 +246,21 @@ function projectType(def) { } /** - * Project a CDDL group into a record. A property with `Occurrence.m === null` is - * an unbounded entry (`* key => value`), not a scalar field: `* text => any` marks - * the record extensible, `* text => T` becomes a typed map, and an unbounded group - * spread is folded in. Everything else is a normal field. + * Project a CDDL group into a record. A property with an unbounded occurrence (`*`/`+`) + * is a map/spread entry, not a scalar field: `* text => any` marks the record extensible, + * `* text => T` becomes a typed map, and an unbounded group spread is folded in. Everything + * else is a normal field. */ function projectRecord(def) { const record = { kind: 'record', fields: [] } for (const prop of (def.Properties ?? []).flat()) { if (!prop || typeof prop !== 'object') continue - // `m === null` is overloaded in this parser: a key-typed entry is a map - // (`* text => value`); an anonymous entry is a structural spread; everything - // else is just an optional field (the `?` quantifier). Only the first two - // are not real fields. - if (prop.Occurrence?.m === null && (!prop.Name || prop.Name in PRIMITIVES || prop.Name in PRELUDE)) { + // An unbounded upper bound is overloaded in this parser: a key-typed entry is a map + // (`* text => value`); an anonymous entry is a structural spread; everything else is + // just an optional field (the `?` quantifier). Only the first two are not real fields. + // The parser emits the bound as Infinity; the AST's JSON round-trip turns it into null, + // so accept either rather than depending on that coercion. + if (isUnbounded(prop.Occurrence) && (!prop.Name || prop.Name in PRIMITIVES || prop.Name in PRELUDE)) { if (prop.Name in PRIMITIVES || prop.Name in PRELUDE) { const value = projectRef(prop.Type) if (value.primitive === 'any') record.extensible = true diff --git a/javascript/selenium-webdriver/project_bidi_schema_test.mjs b/javascript/selenium-webdriver/project_bidi_schema_test.mjs index 1fc7aeddeeea6..6420031d65fa8 100644 --- a/javascript/selenium-webdriver/project_bidi_schema_test.mjs +++ b/javascript/selenium-webdriver/project_bidi_schema_test.mjs @@ -88,6 +88,16 @@ describe('projectSchema', () => { assert.equal(open.fields.length, 0) }) + it('treats an unbounded occurrence as extensible whether m is null or Infinity', () => { + // The cddl parser emits the `*` upper bound as Infinity; only the AST's JSON + // round-trip renders it as null. Projecting an AST directly (no round-trip) must + // still recognize it, not emit a `text` field. + const ast = [group('x.RawOpenMap', [field('text', ['any'], { n: 0, m: Infinity })])] + const open = projectSchema(ast, {}).types['x.RawOpenMap'] + assert.equal(open.extensible, true) + assert.equal(open.fields.length, 0) + }) + it('passes both validators on a well-formed schema', () => { assert.deepEqual(checkSchema(schema), []) assert.deepEqual(checkCompleteness(AST, schema), [])