Skip to content
Merged
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
4 changes: 2 additions & 2 deletions javascript/selenium-webdriver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 8 additions & 4 deletions javascript/selenium-webdriver/generate_bidi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -157,16 +157,20 @@ 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) {
generateTypeScript(readJson(args.ast, 'AST'), readJson(args.model, 'model'), args)
} else {
console.error(
'Usage (one stage per invocation):\n' +
' generate_bidi.mjs --cddl <file> --dump-ast <file>\n' +
' generate_bidi.mjs --cddl <file> [--cddl <file>...] --dump-ast <file>\n' +
' generate_bidi.mjs --ast <file> --dump-model <file>\n' +
' generate_bidi.mjs --ast <file> --model <file> --output-dir <dir> [--enhancements <file>] [--spec-version <v>]',
)
Expand Down
71 changes: 16 additions & 55 deletions javascript/selenium-webdriver/private/generate_bidi.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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).
"""
Expand All @@ -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: <output> <input1> [<input2> ...]
# 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,
)

Expand Down
23 changes: 14 additions & 9 deletions javascript/selenium-webdriver/project_bidi_schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions javascript/selenium-webdriver/project_bidi_schema_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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), [])
Expand Down
Loading