blob: dd6f00afa306291f2edfeffa73a734798bb938ca [file] [log] [blame]
import { Base } from "./base.js";
import { Default } from "./default.js";
import { ExtendedAttributes } from "./extended-attributes.js";
import {
unescape,
type_with_extended_attributes,
autoParenter,
getFirstToken,
} from "./helpers.js";
import { argumentNameKeywords, Tokeniser } from "../tokeniser.js";
import { validationError } from "../error.js";
import {
idlTypeIncludesDictionary,
dictionaryIncludesRequiredField,
} from "../validators/helpers.js";
export class Argument extends Base {
/**
* @param {import("../tokeniser.js").Tokeniser} tokeniser
*/
static parse(tokeniser) {
const start_position = tokeniser.position;
/** @type {Base["tokens"]} */
const tokens = {};
const ret = autoParenter(
new Argument({ source: tokeniser.source, tokens }),
);
ret.extAttrs = ExtendedAttributes.parse(tokeniser);
tokens.optional = tokeniser.consume("optional");
ret.idlType = type_with_extended_attributes(tokeniser, "argument-type");
if (!ret.idlType) {
return tokeniser.unconsume(start_position);
}
if (!tokens.optional) {
tokens.variadic = tokeniser.consume("...");
}
tokens.name =
tokeniser.consumeKind("identifier") ||
tokeniser.consume(...argumentNameKeywords);
if (!tokens.name) {
return tokeniser.unconsume(start_position);
}
ret.default = tokens.optional ? Default.parse(tokeniser) : null;
return ret.this;
}
get type() {
return "argument";
}
get optional() {
return !!this.tokens.optional;
}
get variadic() {
return !!this.tokens.variadic;
}
get name() {
return unescape(this.tokens.name.value);
}
/**
* @param {import("../validator.js").Definitions} defs
*/
*validate(defs) {
yield* this.extAttrs.validate(defs);
yield* this.idlType.validate(defs);
const result = idlTypeIncludesDictionary(this.idlType, defs, {
useNullableInner: true,
});
if (result) {
if (this.idlType.nullable) {
const message = `Dictionary arguments cannot be nullable.`;
yield validationError(
this.tokens.name,
this,
"no-nullable-dict-arg",
message,
);
} else if (!this.optional) {
if (
this.parent &&
!dictionaryIncludesRequiredField(result.dictionary, defs) &&
isLastRequiredArgument(this)
) {
const message = `Dictionary argument must be optional if it has no required fields`;
yield validationError(
this.tokens.name,
this,
"dict-arg-optional",
message,
{
autofix: autofixDictionaryArgumentOptionality(this),
},
);
}
} else if (!this.default) {
const message = `Optional dictionary arguments must have a default value of \`{}\`.`;
yield validationError(
this.tokens.name,
this,
"dict-arg-default",
message,
{
autofix: autofixOptionalDictionaryDefaultValue(this),
},
);
}
}
}
/** @param {import("../writer.js").Writer} w */
write(w) {
return w.ts.wrap([
this.extAttrs.write(w),
w.token(this.tokens.optional),
w.ts.type(this.idlType.write(w)),
w.token(this.tokens.variadic),
w.name_token(this.tokens.name, { data: this }),
this.default ? this.default.write(w) : "",
w.token(this.tokens.separator),
]);
}
}
/**
* @param {Argument} arg
*/
function isLastRequiredArgument(arg) {
const list = arg.parent.arguments || arg.parent.list;
const index = list.indexOf(arg);
const requiredExists = list.slice(index + 1).some((a) => !a.optional);
return !requiredExists;
}
/**
* @param {Argument} arg
*/
function autofixDictionaryArgumentOptionality(arg) {
return () => {
const firstToken = getFirstToken(arg.idlType);
arg.tokens.optional = {
...firstToken,
type: "optional",
value: "optional",
};
firstToken.trivia = " ";
autofixOptionalDictionaryDefaultValue(arg)();
};
}
/**
* @param {Argument} arg
*/
function autofixOptionalDictionaryDefaultValue(arg) {
return () => {
arg.default = Default.parse(new Tokeniser(" = {}"));
};
}