Skip to content

Commit 5307958

Browse files
committed
feat(sfcc): forbid legacy Rhino import globals
1 parent 1a0bbf4 commit 5307958

5 files changed

Lines changed: 123 additions & 8 deletions

File tree

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@ That rule stays enabled in the recommended config by default, because it is stil
8484

8585
The new `sfcc` plugin contains the general Rhino/SFCC runtime rules:
8686

87-
| Rule | Description | Default |
88-
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
89-
| `sfcc/no-e4x-syntax` | Disallows JSX/E4X-like tag syntax (e.g. `<a/>`) in SFCC JavaScript to avoid parser ambiguity and unsupported runtime patterns. | `error` |
90-
| `sfcc/no-type-annotations` | Disallows type annotation syntax in JavaScript files (e.g. `const x: string = ...`, `function y(): number {}`). Rhino/E4X may accept it, but it is invalid in standard JavaScript; use JSDoc typing instead. | `error` |
91-
| `sfcc/prefer-const` | Requires `const` for `let` declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts. | `error` |
92-
| `sfcc/rhino-const-compat` | Enforces `let` instead of `const` in Rhino loop-critical contexts (loop headers and declarations inside loop bodies) and supports auto-fix. | `error` |
93-
| `sfcc/rhino-const-conflict` | Detects same-name `const` declarations in nested blocks within the same function (Rhino treats them as function-scoped) and supports auto-fix to `let`. | `error` |
94-
| `sfcc/valid-require-path` | Validates SFCC-compatible `require()` paths (`dw/*`, `cartridgeName/*`, `./*`, `../*`, `*/*`, `~/*`) and supports optional filesystem existence checks. | `error` |
87+
| Rule | Description | Default |
88+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
89+
| `sfcc/no-e4x-syntax` | Disallows JSX/E4X-like tag syntax (e.g. `<a/>`) in SFCC JavaScript to avoid parser ambiguity and unsupported runtime patterns. | `error` |
90+
| `sfcc/no-type-annotations` | Disallows type annotation syntax in JavaScript files (e.g. `const x: string = ...`, `function y(): number {}`). Rhino/E4X may accept it, but it is invalid in standard JavaScript; use JSDoc typing instead. | `error` |
91+
| `sfcc/no-rhino-import-globals` | Disallows legacy Rhino globals `importScript(...)`, `importPackage(...)`, and `importClass(...)`. Use CommonJS `require()` instead. | `error` |
92+
| `sfcc/prefer-const` | Requires `const` for `let` declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts. | `error` |
93+
| `sfcc/rhino-const-compat` | Enforces `let` instead of `const` in Rhino loop-critical contexts (loop headers and declarations inside loop bodies) and supports auto-fix. | `error` |
94+
| `sfcc/rhino-const-conflict` | Detects same-name `const` declarations in nested blocks within the same function (Rhino treats them as function-scoped) and supports auto-fix to `let`. | `error` |
95+
| `sfcc/valid-require-path` | Validates SFCC-compatible `require()` paths (`dw/*`, `cartridgeName/*`, `./*`, `../*`, `*/*`, `~/*`) and supports optional filesystem existence checks. | `error` |
9596

9697
The recommended config intentionally combines these `sfcc/*` rules so `--fix` does not bounce between conflicting suggestions: Rhino-unsafe `const` becomes `let`, while genuinely safe top-level function bindings still become `const`.
9798

@@ -210,6 +211,10 @@ Q: Are type annotations allowed in `.js` files?
210211

211212
A: No. `sfcc/no-type-annotations` reports annotation syntax in JavaScript files (for example `const x: string = "foo"` or `function y(): number {}`). Rhino/E4X may accept this syntax, but `.js` here follows standard JavaScript where it is invalid. Use JSDoc types instead.
212213

214+
Q: Are legacy Rhino import globals allowed?
215+
216+
A: No. `sfcc/no-rhino-import-globals` reports `importScript(...)`, `importPackage(...)`, and `importClass(...)` and points you to CommonJS `require()` instead.
217+
213218
Q: What suggestion is shown for multiline static markup?
214219

215220
A: For static multiline JSX/E4X-like markup, `sfcc/no-e4x-syntax` suggests converting to `XML(\`...\`)`. For dynamic markup (for example with `{value}`), no conversion suggestion is offered.

src/plugins/sfcc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import noE4xSyntax from "./no-e4x-syntax.js"
2+
import noRhinoImportGlobals from "./no-rhino-import-globals.js"
23
import noTypeAnnotations from "./no-type-annotations.js"
34
import preferConst from "./prefer-const.js"
45
import rhinoConstCompat from "./rhino-const-compat.js"
@@ -9,6 +10,7 @@ const sfcc = {
910
rules: {
1011
"no-e4x-syntax": noE4xSyntax,
1112
"no-type-annotations": noTypeAnnotations,
13+
"no-rhino-import-globals": noRhinoImportGlobals,
1214
"prefer-const": preferConst,
1315
"rhino-const-compat": rhinoConstCompat,
1416
"rhino-const-conflict": rhinoConstConflict,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Rule } from "eslint"
2+
3+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
4+
5+
const LEGACY_RHINO_IMPORTS = new Set(["importScript", "importPackage", "importClass"])
6+
7+
function isJavaScriptTarget(filename: string): boolean {
8+
if (filename === "<input>") {
9+
return true
10+
}
11+
12+
return /\.(?:[cm]?js|ds)$/iu.test(filename)
13+
}
14+
15+
const noRhinoImportGlobals: Rule.RuleModule = {
16+
meta: {
17+
type: "problem",
18+
docs: {
19+
description:
20+
"Disallow legacy Rhino globals importScript, importPackage, and importClass in JavaScript files. Use CommonJS require() instead.",
21+
recommended: true,
22+
},
23+
schema: [],
24+
messages: {
25+
forbiddenLegacyImport: "{{name}}() is a legacy Rhino global. Use CommonJS require() instead.",
26+
},
27+
},
28+
create: withSfccSettings((context) => {
29+
if (!isJavaScriptTarget(context.filename)) {
30+
return {}
31+
}
32+
33+
return {
34+
CallExpression(node: Rule.Node) {
35+
const callExpression = node as Rule.Node & {
36+
callee: Rule.Node & { type?: string; name?: string }
37+
}
38+
const callee = callExpression.callee
39+
40+
if (callee.type !== "Identifier" || !LEGACY_RHINO_IMPORTS.has(callee.name ?? "")) {
41+
return
42+
}
43+
44+
context.report({
45+
node: callee,
46+
messageId: "forbiddenLegacyImport",
47+
data: { name: callee.name },
48+
})
49+
},
50+
}
51+
}),
52+
}
53+
54+
export default noRhinoImportGlobals

src/rules/sfcc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Linter } from "eslint"
33
const sfcc: Linter.RulesRecord = {
44
"sfcc/no-e4x-syntax": "error",
55
"sfcc/no-type-annotations": "error",
6+
"sfcc/no-rhino-import-globals": "error",
67
"sfcc/prefer-const": "error",
78
"sfcc/rhino-const-compat": "error",
89
"sfcc/rhino-const-conflict": "error",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import tseslint from "@typescript-eslint/eslint-plugin"
2+
import { ESLint, type Linter } from "eslint"
3+
import { describe, expect, test } from "vite-plus/test"
4+
5+
import { createRecommendedConfig } from "../src/index.js"
6+
7+
const tsRecommended = tseslint.configs["flat/recommended"] as unknown as
8+
| Linter.Config
9+
| Linter.Config[]
10+
11+
const config: Linter.Config[] = [
12+
...(Array.isArray(tsRecommended) ? tsRecommended : [tsRecommended]),
13+
...createRecommendedConfig({
14+
files: ["**/*.js"],
15+
ignores: [],
16+
}),
17+
]
18+
19+
async function lint(code: string, filename = "cartridges/app_sfra/cartridge/scripts/fixture.js") {
20+
const eslint = new ESLint({
21+
overrideConfigFile: true,
22+
overrideConfig: config,
23+
})
24+
25+
const results = await eslint.lintText(code, { filePath: filename })
26+
return results[0]?.messages || []
27+
}
28+
29+
describe("sfcc/no-rhino-import-globals", () => {
30+
test("reports importScript usage", async () => {
31+
const messages = await lint('importScript("scripts/util");')
32+
33+
expect(messages.some((m) => m.ruleId === "sfcc/no-rhino-import-globals")).toBe(true)
34+
})
35+
36+
test("reports importPackage usage", async () => {
37+
const messages = await lint('importPackage("dw.catalog")')
38+
39+
expect(messages.some((m) => m.ruleId === "sfcc/no-rhino-import-globals")).toBe(true)
40+
})
41+
42+
test("reports importClass usage", async () => {
43+
const messages = await lint("importClass(Packages.java.lang.String)")
44+
45+
expect(messages.some((m) => m.ruleId === "sfcc/no-rhino-import-globals")).toBe(true)
46+
})
47+
48+
test("allows CommonJS require", async () => {
49+
const messages = await lint('const Logger = require("dw/system/Logger")')
50+
51+
expect(messages.some((m) => m.ruleId === "sfcc/no-rhino-import-globals")).toBe(false)
52+
})
53+
})

0 commit comments

Comments
 (0)