Skip to content

fix: warn on bidirectional control characters, fix various issues with template expressions #15893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 12, 2025
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
5 changes: 5 additions & 0 deletions .changeset/rare-crews-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: warn on bidirectional control characters
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,14 @@ Attributes should not contain ':' characters to prevent ambiguity with Svelte di
Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes
```

### bidirectional_control_characters

```
A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences
```

Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information.

### bind_invalid_each_rest

```
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/messages/compile-warnings/misc.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## bidirectional_control_characters

> A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences

Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information.

## legacy_code

> `%code%` is no longer valid — please use `%suggestion%` instead
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
Expand All @@ -63,6 +64,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { TemplateElement } from './visitors/TemplateElement.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
Expand Down Expand Up @@ -156,6 +158,7 @@ const visitors = {
KeyBlock,
LabeledStatement,
LetDirective,
Literal,
MemberExpression,
NewExpression,
OnDirective,
Expand All @@ -176,6 +179,7 @@ const visitors = {
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
TemplateElement,
Text,
TransitionDirective,
TitleElement,
Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** @import { Literal } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';

/**
* @param {Literal} node
*/
export function Literal(node) {
if (typeof node.value === 'string') {
if (regex_bidirectional_control_characters.test(node.value)) {
w.bidirectional_control_characters(node);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @import { TemplateElement } from 'estree' */
import * as w from '../../../warnings.js';
import { regex_bidirectional_control_characters } from '../../patterns.js';

/**
* @param {TemplateElement} node
*/
export function TemplateElement(node) {
if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
w.bidirectional_control_characters(node);
}
}
38 changes: 35 additions & 3 deletions packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import { regex_not_whitespace } from '../../patterns.js';
import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';

/**
* @param {AST.Text} node
* @param {Context} context
*/
export function Text(node, context) {
const in_template = context.path.at(-1)?.type === 'Fragment';
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));

if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (
parent.type === 'Fragment' &&
context.state.parent_element &&
regex_not_whitespace.test(node.data)
) {
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
if (message) {
e.node_invalid_placement(node, message);
}
}

regex_bidirectional_control_characters.lastIndex = 0;
for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
let is_ignored = false;

// if we have a svelte-ignore comment earlier in the text, bail
// (otherwise we can only use svelte-ignore on parent elements/blocks)
if (parent.type === 'Fragment') {
for (const child of parent.nodes) {
if (child === node) break;

if (child.type === 'Comment') {
is_ignored ||= extract_svelte_ignore(
child.start + 4,
child.data,
context.state.analysis.runes
).includes('bidirectional_control_characters');
}
}
}

if (!is_ignored) {
let start = match.index + node.start;
w.bidirectional_control_characters({ start, end: start + match[0].length });
}
}
}
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export const regex_invalid_identifier_chars = /(^[^a-zA-Z_$]|[^a-zA-Z0-9_$])/g;
export const regex_starts_with_vowel = /^[aeiou]/;
export const regex_heading_tags = /^h[1-6]$/;
export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/;
export const regex_bidirectional_control_characters =
/[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g;
9 changes: 9 additions & 0 deletions packages/svelte/src/compiler/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const codes = [
'a11y_role_supports_aria_props_implicit',
'a11y_unknown_aria_attribute',
'a11y_unknown_role',
'bidirectional_control_characters',
'legacy_code',
'unknown_code',
'options_deprecated_accessors',
Expand Down Expand Up @@ -506,6 +507,14 @@ export function a11y_unknown_role(node, role, suggestion) {
w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
}

/**
* A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences
* @param {null | NodeLike} node
*/
export function bidirectional_control_characters(node) {
w(node, 'bidirectional_control_characters', `A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences\nhttps://svelte.dev/e/bidirectional_control_characters`);
}

/**
* `%code%` is no longer valid — please use `%suggestion%` instead
* @param {null | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
let name = '\u2067\u2066rld\u2069\u2066wo\u2069\u2069';
</script>
⁧⁦def⁩⁦abc⁩⁩
<h1>Hello, {name}!</h1>

<!-- svelte-ignore bidirectional_control_characters -->
⁧⁦def⁩⁦abc⁩⁩
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"code": "bidirectional_control_characters",
"message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 58
}
},
{
"code": "bidirectional_control_characters",
"message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
"start": {
"line": 4,
"column": 0
},
"end": {
"line": 4,
"column": 2
}
},
{
"code": "bidirectional_control_characters",
"message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
"start": {
"line": 4,
"column": 5
},
"end": {
"line": 4,
"column": 7
}
},
{
"code": "bidirectional_control_characters",
"message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences",
"start": {
"line": 4,
"column": 10
},
"end": {
"line": 4,
"column": 12
}
}
]