Skip to content

Commit 2e5b656

Browse files
authored
feat(lint): add noDocumentCookie rule (#4204)
1 parent 2e5e3f2 commit 2e5b656

File tree

13 files changed

+429
-82
lines changed

13 files changed

+429
-82
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b
4343

4444
### Linter
4545

46+
#### New features
47+
48+
- Add [noDocumentCookie](https://biomejs.dev/linter/rules/no-document-cookie/). Contributed by @tunamaguro
49+
4650
#### Bug Fixes
4751

4852
- Biome no longer crashes when it encounters a string that contain a multibyte character ([#4181](https://github.com/biomejs/biome/issues/4181)).

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

+101-82
Large diffs are not rendered by default.

crates/biome_diagnostics_categories/src/categories.rs

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ define_categories! {
137137
"lint/nursery/noCommonJs": "https://biomejs.dev/linter/rules/no-common-js",
138138
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
139139
"lint/nursery/noDescendingSpecificity": "https://biomejs.dev/linter/rules/no-descending-specificity",
140+
"lint/nursery/noDocumentCookie": "https://biomejs.dev/linter/rules/no-document-cookie",
140141
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
141142
"lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules",
142143
"lint/nursery/noDuplicateCustomProperties": "https://biomejs.dev/linter/rules/no-duplicate-custom-properties",

crates/biome_js_analyze/src/lint/nursery.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use biome_analyze::declare_lint_group;
44

55
pub mod no_common_js;
6+
pub mod no_document_cookie;
67
pub mod no_duplicate_else_if;
78
pub mod no_dynamic_namespace_import_access;
89
pub mod no_enum;
@@ -39,6 +40,7 @@ declare_lint_group! {
3940
name : "nursery" ,
4041
rules : [
4142
self :: no_common_js :: NoCommonJs ,
43+
self :: no_document_cookie :: NoDocumentCookie ,
4244
self :: no_duplicate_else_if :: NoDuplicateElseIf ,
4345
self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess ,
4446
self :: no_enum :: NoEnum ,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
2+
use biome_console::markup;
3+
use biome_js_semantic::SemanticModel;
4+
use biome_js_syntax::{
5+
global_identifier, AnyJsAssignment, AnyJsExpression, JsAssignmentExpression,
6+
};
7+
use biome_rowan::AstNode;
8+
9+
use crate::services::semantic::Semantic;
10+
11+
declare_lint_rule! {
12+
/// Disallow direct assignments to `document.cookie`.
13+
///
14+
/// It's not recommended to use document.cookie directly as it's easy to get the string wrong.
15+
/// Instead, you should use the [Cookie Store API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore).
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```js,expect_diagnostic
22+
/// document.cookie = "foo=bar";
23+
/// ```
24+
///
25+
/// ```js,expect_diagnostic
26+
/// document.cookie += "; foo=bar";
27+
/// ```
28+
///
29+
/// ### Valid
30+
///
31+
/// ```js
32+
/// const array = document.cookie.split("; ");
33+
/// ```
34+
///
35+
/// ```js
36+
/// await cookieStore
37+
/// .set({
38+
/// name: "foo",
39+
/// value: "bar",
40+
/// expires: Date.now() + 24 * 60 * 60,
41+
/// domain: "example.com",
42+
/// })
43+
/// ```
44+
///
45+
/// ```js
46+
/// import Cookies from 'js-cookie';
47+
///
48+
/// Cookies.set('foo', 'bar');
49+
/// ```
50+
///
51+
pub NoDocumentCookie {
52+
version: "next",
53+
name: "noDocumentCookie",
54+
language: "js",
55+
recommended: false,
56+
sources: &[RuleSource::EslintUnicorn("no-document-cookie")],
57+
}
58+
}
59+
60+
/// Check `expr` is `document`
61+
fn is_global_document(expr: &AnyJsExpression, model: &SemanticModel) -> Option<()> {
62+
let (reference, name) = global_identifier(expr)?;
63+
64+
// Check identifier is `document`
65+
if name.text() != "document" {
66+
return None;
67+
};
68+
69+
// TODO: Verify that the variable is assigned the global `document` to be closer to the original rule.
70+
model.binding(&reference).is_none().then_some(())
71+
}
72+
73+
/// Check member is `cookie`
74+
fn is_cookie(assignment: &AnyJsAssignment) -> Option<()> {
75+
const COOKIE: &str = "cookie";
76+
match assignment {
77+
// `document.cookie`
78+
AnyJsAssignment::JsStaticMemberAssignment(static_assignment) => {
79+
let property = static_assignment.member().ok()?;
80+
81+
if property.text() != COOKIE {
82+
return None;
83+
};
84+
}
85+
// `document["cookie"]`
86+
AnyJsAssignment::JsComputedMemberAssignment(computed_assignment) => {
87+
let any_expr = computed_assignment.member().ok()?;
88+
let string_literal = any_expr
89+
.as_any_js_literal_expression()?
90+
.as_js_string_literal_expression()?;
91+
let inner_string = string_literal.inner_string_text().ok()?;
92+
93+
if inner_string.text() != COOKIE {
94+
return None;
95+
}
96+
}
97+
_ => {
98+
return None;
99+
}
100+
}
101+
102+
Some(())
103+
}
104+
105+
impl Rule for NoDocumentCookie {
106+
type Query = Semantic<JsAssignmentExpression>;
107+
type State = ();
108+
type Signals = Option<Self::State>;
109+
type Options = ();
110+
111+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
112+
let node = ctx.query();
113+
let left = node.left().ok()?;
114+
115+
let any_assignment = left.as_any_js_assignment()?;
116+
117+
let expr = match any_assignment {
118+
AnyJsAssignment::JsStaticMemberAssignment(assignment) => assignment.object().ok()?,
119+
AnyJsAssignment::JsComputedMemberAssignment(assignment) => assignment.object().ok()?,
120+
_ => {
121+
return None;
122+
}
123+
};
124+
125+
is_global_document(&expr, ctx.model())?;
126+
127+
is_cookie(any_assignment)?;
128+
129+
Some(())
130+
}
131+
132+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
133+
let node = ctx.query();
134+
Some(
135+
RuleDiagnostic::new(
136+
rule_category!(),
137+
node.range(),
138+
markup! {
139+
"Direct assigning to "<Emphasis>"document.cookie"</Emphasis>" is not recommended."
140+
},
141+
)
142+
.note(markup! {
143+
"Consider using the "<Hyperlink href = "https://developer.mozilla.org/en-US/docs/Web/API/CookieStore">"Cookie Store API"</Hyperlink>"."
144+
}),
145+
)
146+
}
147+
}

crates/biome_js_analyze/src/options.rs

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
document.cookie = "foo=bar";
2+
document.cookie += ";foo=bar"
3+
4+
window.document.cookie = "foo=bar";
5+
globalThis.window.document.cookie = "foo=bar";
6+
7+
document["cookie"] = "foo=bar"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```jsx
7+
document.cookie = "foo=bar";
8+
document.cookie += ";foo=bar"
9+
10+
window.document.cookie = "foo=bar";
11+
globalThis.window.document.cookie = "foo=bar";
12+
13+
document["cookie"] = "foo=bar"
14+
```
15+
16+
# Diagnostics
17+
```
18+
invalid.js:1:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19+
20+
! Direct assigning to document.cookie is not recommended.
21+
22+
> 1 │ document.cookie = "foo=bar";
23+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
24+
2 │ document.cookie += ";foo=bar"
25+
3 │
26+
27+
i Consider using the Cookie Store API.
28+
29+
30+
```
31+
32+
```
33+
invalid.js:2:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34+
35+
! Direct assigning to document.cookie is not recommended.
36+
37+
1 │ document.cookie = "foo=bar";
38+
> 2 │ document.cookie += ";foo=bar"
39+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
3 │
41+
4 │ window.document.cookie = "foo=bar";
42+
43+
i Consider using the Cookie Store API.
44+
45+
46+
```
47+
48+
```
49+
invalid.js:4:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50+
51+
! Direct assigning to document.cookie is not recommended.
52+
53+
2 │ document.cookie += ";foo=bar"
54+
3 │
55+
> 4 │ window.document.cookie = "foo=bar";
56+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57+
5 │ globalThis.window.document.cookie = "foo=bar";
58+
6 │
59+
60+
i Consider using the Cookie Store API.
61+
62+
63+
```
64+
65+
```
66+
invalid.js:5:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
67+
68+
! Direct assigning to document.cookie is not recommended.
69+
70+
4 │ window.document.cookie = "foo=bar";
71+
> 5 │ globalThis.window.document.cookie = "foo=bar";
72+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73+
6 │
74+
7 │ document["cookie"] = "foo=bar"
75+
76+
i Consider using the Cookie Store API.
77+
78+
79+
```
80+
81+
```
82+
invalid.js:7:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83+
84+
! Direct assigning to document.cookie is not recommended.
85+
86+
5 │ globalThis.window.document.cookie = "foo=bar";
87+
6 │
88+
> 7 │ document["cookie"] = "foo=bar"
89+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90+
91+
i Consider using the Cookie Store API.
92+
93+
94+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
document.cookie
2+
3+
const foo = document.cookie;
4+
5+
const array = document.cookie.split("; ");
6+
7+
cookieStore
8+
.set({
9+
name: "foo",
10+
value: "bar",
11+
expires: Date.now() + 24 * 60 * 60,
12+
domain: "example.com",
13+
})
14+
15+
function document_is_not_global1(document){
16+
document.cookie = "bar=foo"
17+
}
18+
19+
function document_is_not_global2(){
20+
const document = "foo";
21+
document.cookie = "bar=foo"
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: valid.js
4+
---
5+
# Input
6+
```jsx
7+
document.cookie
8+
9+
const foo = document.cookie;
10+
11+
const array = document.cookie.split("; ");
12+
13+
cookieStore
14+
.set({
15+
name: "foo",
16+
value: "bar",
17+
expires: Date.now() + 24 * 60 * 60,
18+
domain: "example.com",
19+
})
20+
21+
function document_is_not_global1(document){
22+
document.cookie = "bar=foo"
23+
}
24+
25+
function document_is_not_global2(){
26+
const document = "foo";
27+
document.cookie = "bar=foo"
28+
}
29+
```

packages/@biomejs/backend-jsonrpc/src/workspace.ts

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@biomejs/biome/configuration_schema.json

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)