symfony/css-selector · error · ExpressionErrorException
Expected a single string or identifier for :contains(), got
Error message
Expected a single string or identifier for :contains(), got
What it means
ExpressionErrorException thrown by FunctionExtension::translateContains when any argument token of :contains() is neither a string nor an identifier token. :contains() translates to an XPath contains(string(.), ...) condition that only accepts literal values, so any other token type (e.g. a dimension, number, hash, or operator token) is rejected.
Solutions
- Quote the argument as a string: 'div:contains("42")' instead of 'div:contains(42)'.
- Use a bare identifier (letters) if a quoted string is not required: 'div:contains(Warning)'.
- Catch ExpressionErrorException around cssToXPath and surface a clear invalid-selector message.
- Pre-validate :contains() arguments in generated selectors to ensure they are quoted strings or plain identifiers.
Example fix
// before
'div:contains(42)'
// after
'div:contains("42")' Defensive patterns
Strategy: validation
Validate before calling
foreach ($args as $arg) {
if (!preg_match('/^("[^"]*"|\'[\w\s-]*\'|[A-Za-z][\w-]*)$/', $arg)) {
throw new \InvalidArgumentException(':contains() argument must be a quoted string or identifier.');
}
} Try / catch
try {
$xpath = $translator->cssToXPath($css);
} catch (ExpressionErrorException $e) {
if (str_contains($e->getMessage(), ':contains()')) {
// reject selector
}
} Prevention
- Always quote :contains() text values.
- Keep :contains() to a single string/identifier argument.
- Escape embedded quotes in generated selectors.
- Avoid passing numeric values without quotes.
When it happens
Trigger: Translating selectors like 'div:contains(42)' where 42 parses as a number token, 'div:contains("a" "b")' with multiple non-identifier arguments, or 'div:contains()' referencing a token of the wrong type at index 0 when building the condition.
Common situations: Programmatic selector construction that passes numeric values unquoted, mistyping the argument so the tokenizer classifies it as a number/percentage, injection of unusual characters that change token classification.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Expected a single string or identifier for :lang(), got
- Expected a single string or identifier for :lang(), got
- Got too deeply nested
- Invalid series: " ".
- "*:nth-of-type()" is not implemented.
AI-assisted analysis of symfony/css-selector@08e2905152 (2026-09-14).
Data as JSON: /api/errors/d320cce420565a0d.
Report an issue: GitHub.
Appendix: source
Thrown at XPath/Extension/FunctionExtension.php:133
*/
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
}
return $this->translateNthChild($xpath, $function, true, false);
}
/**
* @throws ExpressionErrorException
*/
public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments));
}
}
return $xpath->addCondition(\sprintf(
'contains(string(.), %s)',
Translator::getXpathLiteral($arguments[0]->getValue())
));
}
/**
* @throws ExpressionErrorException
*/
public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
if (!($token->isString() || $token->isIdentifier())) {
throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments));View on GitHub (pinned to 08e2905152)