diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d22fe0..73b4322 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#84](https://github.com/green-code-initiative/creedengo-javascript/pull/84) Add rule GCI535 "No imported number format library"
+### Changed
+
+- [#113](https://github.com/green-code-initiative/creedengo-javascript/pull/113) Extend rule GCI530 "no-torch" to detect HTML5 Web API usage
+
## [3.1.0] - 2026-05-10
### Added
diff --git a/eslint-plugin/docs/rules/no-torch.md b/eslint-plugin/docs/rules/no-torch.md
index d67cb7e..2d92a14 100644
--- a/eslint-plugin/docs/rules/no-torch.md
+++ b/eslint-plugin/docs/rules/no-torch.md
@@ -12,12 +12,22 @@ As a developer, you should avoid programmatically enabling torch mode.
The flashlight can significantly drain the device's battery. If it is turned on without the user's knowledge, it could lead to unwanted battery consumption.
+### React Native
+
```js
import Torch from "react-native-torch"; // Not-compliant
+
+import axios from "axios"; // Compliant
```
+### HTML5 Web API (MediaTrackConstraints)
+
```js
-import axios from "axios"; // Compliant
+// Not-compliant
+await track.applyConstraints({ advanced: [{ torch: true }] });
+
+// Compliant
+await track.applyConstraints({ advanced: [{ facingMode: "environment" }] });
```
## Resources
@@ -25,3 +35,4 @@ import axios from "axios"; // Compliant
### Documentation
- [CNUMR best practices mobile](https://github.com/cnumr/best-practices-mobile) - Torch free
+- [MediaTrackConstraints: torch property (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#torch)
diff --git a/eslint-plugin/lib/rules/no-torch.js b/eslint-plugin/lib/rules/no-torch.js
index 65d924e..7d067d1 100644
--- a/eslint-plugin/lib/rules/no-torch.js
+++ b/eslint-plugin/lib/rules/no-torch.js
@@ -18,6 +18,25 @@
"use strict";
+const getPropertyValue = (propName) => (obj) => {
+ if (obj.type !== "ObjectExpression") return null;
+ return obj.properties.find((p) => {
+ if (p.type !== "Property") return false;
+ const name = p.key.type === "Identifier" ? p.key.name : p.key.value;
+ return name === propName;
+ })?.value;
+};
+
+const hasTorchTrueInAdvanced = (arg) => {
+ const advanced = getPropertyValue("advanced")(arg);
+ if (!advanced || advanced.type !== "ArrayExpression") return false;
+
+ return advanced.elements.some((el) => {
+ const torch = getPropertyValue("torch")(el);
+ return torch?.type === "Literal" && torch.value === true;
+ });
+};
+
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
@@ -38,8 +57,27 @@ module.exports = {
return {
ImportDeclaration(node) {
- const currentLibrary = node.source.value;
- if (currentLibrary === reactNativeTorchLibrary) {
+ if (node.source.value === reactNativeTorchLibrary) {
+ context.report({
+ node,
+ messageId: "ShouldNotProgrammaticallyEnablingTorchMode",
+ });
+ }
+ },
+
+ CallExpression(node) {
+ const { callee } = node;
+
+ const isApplyConstraints =
+ callee.type === "MemberExpression" &&
+ ((callee.computed &&
+ callee.property.type === "Literal" &&
+ callee.property.value === "applyConstraints") ||
+ (!callee.computed &&
+ callee.property.name === "applyConstraints")) &&
+ node.arguments.length > 0;
+
+ if (isApplyConstraints && hasTorchTrueInAdvanced(node.arguments[0])) {
context.report({
node,
messageId: "ShouldNotProgrammaticallyEnablingTorchMode",
diff --git a/eslint-plugin/tests/lib/rules/no-torch.test.js b/eslint-plugin/tests/lib/rules/no-torch.test.js
index 531f84d..72a44c5 100644
--- a/eslint-plugin/tests/lib/rules/no-torch.test.js
+++ b/eslint-plugin/tests/lib/rules/no-torch.test.js
@@ -42,16 +42,75 @@ const expectedError = {
const tests = {
valid: [
- `
- import axios from 'axios';
- `,
+ // Import tests
+ `import axios from 'axios';`,
+ `import * as torch from 'other-package';`,
+ `import { torch } from 'other-package';`,
+
+ // applyConstraints with torch: false
+ `track.applyConstraints({ advanced: [{ torch: false }] });`,
+ `track.applyConstraints({ advanced: [{ torch: false }, { facingMode: 'user' }] });`,
+
+ // applyConstraints without torch property
+ `track.applyConstraints({ advanced: [{ facingMode: 'environment' }] });`,
+ `track.applyConstraints({ advanced: [{ facingMode: 'environment' }, { width: 640 }] });`,
+
+ // applyConstraints with empty advanced
+ `track.applyConstraints({ advanced: [] });`,
+
+ // applyConstraints with standard constraints (no advanced)
+ `track.applyConstraints({ width: 1280, height: 720 });`,
+
+ // Non-applyConstraints methods
+ `doSomething({ advanced: [{ torch: true }] });`,
],
invalid: [
+ // Import react-native-torch
{
code: "import Torch from 'react-native-torch';",
errors: [expectedError],
},
+ {
+ code: "import { torch } from 'react-native-torch';",
+ errors: [expectedError],
+ },
+ {
+ code: "import * as ReactNativeTorch from 'react-native-torch';",
+ errors: [expectedError],
+ },
+
+ // applyConstraints with torch: true
+ {
+ code: "track.applyConstraints({ advanced: [{ torch: true }] });",
+ errors: [expectedError],
+ },
+
+ // torch: true with other properties in advanced
+ {
+ code: "track.applyConstraints({ advanced: [{ facingMode: 'environment' }, { torch: true }] });",
+ errors: [expectedError],
+ },
+ {
+ code: "track.applyConstraints({ advanced: [{ torch: true }, { facingMode: 'user' }] });",
+ errors: [expectedError],
+ },
+ {
+ code: "track.applyConstraints({ advanced: [{ width: 640 }, { torch: true }, { facingMode: 'environment' }] });",
+ errors: [expectedError],
+ },
+
+ // Multiple torch: true entries
+ {
+ code: "track.applyConstraints({ advanced: [{ torch: true }, { torch: true }] });",
+ errors: [expectedError],
+ },
+
+ // torch: true with extra properties in same object
+ {
+ code: "track.applyConstraints({ advanced: [{ torch: true, width: 640 }] });",
+ errors: [expectedError],
+ },
],
};
diff --git a/sonar-plugin/pom.xml b/sonar-plugin/pom.xml
index 712eec7..2e856ae 100644
--- a/sonar-plugin/pom.xml
+++ b/sonar-plugin/pom.xml
@@ -48,7 +48,7 @@
${encoding}
${encoding}
- 3.0.0
+ 3.1.0
13.0.0.3026
11.8.0.37897
1.25.1.3002
diff --git a/test-project/src/no-torch.js b/test-project/src/no-torch.js
index 3e6b55e..f4ad0bf 100644
--- a/test-project/src/no-torch.js
+++ b/test-project/src/no-torch.js
@@ -1,3 +1,18 @@
+// React Native
import Torch from "react-native-torch"; // Non-compliant: torch should not be enabled
Torch.switchState(true);
+
+// Web API (MediaTrackConstraints)
+export async function example() {
+ const mediaStream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ });
+ const [track] = mediaStream.getVideoTracks();
+
+ await track.applyConstraints({ advanced: [{ torch: true }] }); // Non-compliant: programmatically enables torch via advanced constraints
+
+ await track.applyConstraints({ advanced: [{ torch: false }] }); // Compliant: programmatically disables torch via advanced constraints
+
+ await track.applyConstraints({ advanced: [{ facingMode: "environment" }] }); // Compliant: no torch constraint
+}
diff --git a/test-project/yarn.lock b/test-project/yarn.lock
index 4cd76f6..e25d90d 100644
--- a/test-project/yarn.lock
+++ b/test-project/yarn.lock
@@ -21,10 +21,10 @@ __metadata:
"@creedengo/eslint-plugin@file:../eslint-plugin::locator=creedengo-javascript-test-project%40workspace%3A.":
version: 3.1.0
- resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=30e64e&locator=creedengo-javascript-test-project%40workspace%3A."
+ resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=5bb3d4&locator=creedengo-javascript-test-project%40workspace%3A."
peerDependencies:
eslint: ^9.0.0 || ^10.0.0
- checksum: 10c0/35357906578cb26b758f44fb8fdb12656de3a6d3297d97002f3a1c755066b81e1321badd1f4a01a9e9d156b0545b5611774c2ed83e80600168c134f0dc0846fd
+ checksum: 10c0/bfe290b4ff43b71e4adcd9713fbd8f2fed54b34f5ca13db3c0d81225114f84a61bcb08d6756b2cedbdfc0ef19f2442011c1b771d2eae3885b031f74977ee03fc
languageName: node
linkType: hard