Skip to content

Commit

Permalink
feat(lint): add noDocumentCookie rule (#4204)
Browse files Browse the repository at this point in the history
  • Loading branch information
tunamaguro authored Oct 10, 2024
1 parent 2e5e3f2 commit 2e5b656
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 82 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

### Linter

#### New features

- Add [noDocumentCookie](https://biomejs.dev/linter/rules/no-document-cookie/). Contributed by @tunamaguro

#### Bug Fixes

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 101 additions & 82 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ define_categories! {
"lint/nursery/noCommonJs": "https://biomejs.dev/linter/rules/no-common-js",
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
"lint/nursery/noDescendingSpecificity": "https://biomejs.dev/linter/rules/no-descending-specificity",
"lint/nursery/noDocumentCookie": "https://biomejs.dev/linter/rules/no-document-cookie",
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
"lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules",
"lint/nursery/noDuplicateCustomProperties": "https://biomejs.dev/linter/rules/no-duplicate-custom-properties",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use biome_analyze::declare_lint_group;

pub mod no_common_js;
pub mod no_document_cookie;
pub mod no_duplicate_else_if;
pub mod no_dynamic_namespace_import_access;
pub mod no_enum;
Expand Down Expand Up @@ -39,6 +40,7 @@ declare_lint_group! {
name : "nursery" ,
rules : [
self :: no_common_js :: NoCommonJs ,
self :: no_document_cookie :: NoDocumentCookie ,
self :: no_duplicate_else_if :: NoDuplicateElseIf ,
self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess ,
self :: no_enum :: NoEnum ,
Expand Down
147 changes: 147 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_document_cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
global_identifier, AnyJsAssignment, AnyJsExpression, JsAssignmentExpression,
};
use biome_rowan::AstNode;

use crate::services::semantic::Semantic;

declare_lint_rule! {
/// Disallow direct assignments to `document.cookie`.
///
/// It's not recommended to use document.cookie directly as it's easy to get the string wrong.
/// Instead, you should use the [Cookie Store API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore).
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// document.cookie = "foo=bar";
/// ```
///
/// ```js,expect_diagnostic
/// document.cookie += "; foo=bar";
/// ```
///
/// ### Valid
///
/// ```js
/// const array = document.cookie.split("; ");
/// ```
///
/// ```js
/// await cookieStore
/// .set({
/// name: "foo",
/// value: "bar",
/// expires: Date.now() + 24 * 60 * 60,
/// domain: "example.com",
/// })
/// ```
///
/// ```js
/// import Cookies from 'js-cookie';
///
/// Cookies.set('foo', 'bar');
/// ```
///
pub NoDocumentCookie {
version: "next",
name: "noDocumentCookie",
language: "js",
recommended: false,
sources: &[RuleSource::EslintUnicorn("no-document-cookie")],
}
}

/// Check `expr` is `document`
fn is_global_document(expr: &AnyJsExpression, model: &SemanticModel) -> Option<()> {
let (reference, name) = global_identifier(expr)?;

// Check identifier is `document`
if name.text() != "document" {
return None;
};

// TODO: Verify that the variable is assigned the global `document` to be closer to the original rule.
model.binding(&reference).is_none().then_some(())
}

/// Check member is `cookie`
fn is_cookie(assignment: &AnyJsAssignment) -> Option<()> {
const COOKIE: &str = "cookie";
match assignment {
// `document.cookie`
AnyJsAssignment::JsStaticMemberAssignment(static_assignment) => {
let property = static_assignment.member().ok()?;

if property.text() != COOKIE {
return None;
};
}
// `document["cookie"]`
AnyJsAssignment::JsComputedMemberAssignment(computed_assignment) => {
let any_expr = computed_assignment.member().ok()?;
let string_literal = any_expr
.as_any_js_literal_expression()?
.as_js_string_literal_expression()?;
let inner_string = string_literal.inner_string_text().ok()?;

if inner_string.text() != COOKIE {
return None;
}
}
_ => {
return None;
}
}

Some(())
}

impl Rule for NoDocumentCookie {
type Query = Semantic<JsAssignmentExpression>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let left = node.left().ok()?;

let any_assignment = left.as_any_js_assignment()?;

let expr = match any_assignment {
AnyJsAssignment::JsStaticMemberAssignment(assignment) => assignment.object().ok()?,
AnyJsAssignment::JsComputedMemberAssignment(assignment) => assignment.object().ok()?,
_ => {
return None;
}
};

is_global_document(&expr, ctx.model())?;

is_cookie(any_assignment)?;

Some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Direct assigning to "<Emphasis>"document.cookie"</Emphasis>" is not recommended."
},
)
.note(markup! {
"Consider using the "<Hyperlink href = "https://developer.mozilla.org/en-US/docs/Web/API/CookieStore">"Cookie Store API"</Hyperlink>"."
}),
)
}
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
document.cookie = "foo=bar";
document.cookie += ";foo=bar"

window.document.cookie = "foo=bar";
globalThis.window.document.cookie = "foo=bar";

document["cookie"] = "foo=bar"
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.js
---
# Input
```jsx
document.cookie = "foo=bar";
document.cookie += ";foo=bar"

window.document.cookie = "foo=bar";
globalThis.window.document.cookie = "foo=bar";

document["cookie"] = "foo=bar"
```

# Diagnostics
```
invalid.js:1:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Direct assigning to document.cookie is not recommended.
> 1 │ document.cookie = "foo=bar";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 │ document.cookie += ";foo=bar"
3 │
i Consider using the Cookie Store API.
```

```
invalid.js:2:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Direct assigning to document.cookie is not recommended.
1 │ document.cookie = "foo=bar";
> 2 │ document.cookie += ";foo=bar"
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │
4 │ window.document.cookie = "foo=bar";
i Consider using the Cookie Store API.
```

```
invalid.js:4:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Direct assigning to document.cookie is not recommended.
2 │ document.cookie += ";foo=bar"
3 │
> 4 │ window.document.cookie = "foo=bar";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 │ globalThis.window.document.cookie = "foo=bar";
6 │
i Consider using the Cookie Store API.
```

```
invalid.js:5:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Direct assigning to document.cookie is not recommended.
4 │ window.document.cookie = "foo=bar";
> 5 │ globalThis.window.document.cookie = "foo=bar";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 │
7 │ document["cookie"] = "foo=bar"
i Consider using the Cookie Store API.
```

```
invalid.js:7:1 lint/nursery/noDocumentCookie ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Direct assigning to document.cookie is not recommended.
5 │ globalThis.window.document.cookie = "foo=bar";
6 │
> 7 │ document["cookie"] = "foo=bar"
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
i Consider using the Cookie Store API.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
document.cookie

const foo = document.cookie;

const array = document.cookie.split("; ");

cookieStore
.set({
name: "foo",
value: "bar",
expires: Date.now() + 24 * 60 * 60,
domain: "example.com",
})

function document_is_not_global1(document){
document.cookie = "bar=foo"
}

function document_is_not_global2(){
const document = "foo";
document.cookie = "bar=foo"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.js
---
# Input
```jsx
document.cookie

const foo = document.cookie;

const array = document.cookie.split("; ");

cookieStore
.set({
name: "foo",
value: "bar",
expires: Date.now() + 24 * 60 * 60,
domain: "example.com",
})

function document_is_not_global1(document){
document.cookie = "bar=foo"
}

function document_is_not_global2(){
const document = "foo";
document.cookie = "bar=foo"
}
```
5 changes: 5 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2e5b656

Please sign in to comment.