From dd4224b2416cf414a50d51f7f5b5b41af5512e86 Mon Sep 17 00:00:00 2001 From: "Hugo.Mo" Date: Tue, 19 May 2026 17:22:33 +0200 Subject: [PATCH 1/6] Extend rule GCI530 "no-torch" to detect HTML5 Web API usage --- CHANGELOG.md | 4 ++ eslint-plugin/docs/rules/no-torch.md | 13 ++++- eslint-plugin/lib/rules/no-torch.js | 57 +++++++++++++++++-- .../tests/lib/rules/no-torch.test.js | 19 ++++++- test-project/src/no-torch.js | 13 +++++ test-project/yarn.lock | 4 +- 6 files changed, 100 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d22fe0..b969d17 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 + +- [#48](https://github.com/green-code-initiative/creedengo-javascript/issues/48) Extend rule GCI530 "no-torch" to detect HTML5 Web API usage (`MediaTrackConstraints` torch constraint via `applyConstraints`) + ## [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..e698e84 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..8c824ca 100644 --- a/eslint-plugin/lib/rules/no-torch.js +++ b/eslint-plugin/lib/rules/no-torch.js @@ -18,6 +18,46 @@ "use strict"; +const reactNativeTorchLibrary = "react-native-torch"; + +function getPropertyName(prop) { + if (prop.key.type === "Identifier") return prop.key.name; + if (prop.key.type === "Literal") return String(prop.key.value); + return null; +} + +function findProperty(objectExpression, name) { + return objectExpression.properties.find( + (p) => p.type === "Property" && getPropertyName(p) === name + ); +} + +function objectHasTorchProperty(objectExpression) { + return Boolean(findProperty(objectExpression, "torch")); +} + +function advancedArrayHasTorch(arrayExpression) { + return arrayExpression.elements.some( + (el) => el && el.type === "ObjectExpression" && objectHasTorchProperty(el) + ); +} + +function constraintsArgUsesTorchInAdvanced(arg) { + if (arg.type !== "ObjectExpression") return false; + const advancedProp = findProperty(arg, "advanced"); + if (!advancedProp || advancedProp.value.type !== "ArrayExpression") return false; + return advancedArrayHasTorch(advancedProp.value); +} + +function isApplyConstraintsCall(node) { + const { callee } = node; + if (callee.type !== "MemberExpression") return false; + const methodName = callee.computed + ? callee.property.type === "Literal" && callee.property.value + : callee.property.name; + return methodName === "applyConstraints" && node.arguments.length > 0; +} + /** @type {import("eslint").Rule.RuleModule} */ module.exports = { meta: { @@ -34,12 +74,21 @@ module.exports = { schema: [], }, create: function (context) { - const reactNativeTorchLibrary = "react-native-torch"; - return { ImportDeclaration(node) { - const currentLibrary = node.source.value; - if (currentLibrary === reactNativeTorchLibrary) { + if (node.source.value === reactNativeTorchLibrary) { + context.report({ + node, + messageId: "ShouldNotProgrammaticallyEnablingTorchMode", + }); + } + }, + + CallExpression(node) { + if ( + isApplyConstraintsCall(node) && + constraintsArgUsesTorchInAdvanced(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..5f628ea 100644 --- a/eslint-plugin/tests/lib/rules/no-torch.test.js +++ b/eslint-plugin/tests/lib/rules/no-torch.test.js @@ -42,9 +42,10 @@ const expectedError = { const tests = { valid: [ - ` - import axios from 'axios'; - `, + `import axios from 'axios';`, + `track.applyConstraints({ advanced: [{ facingMode: 'environment' }] });`, + `track.applyConstraints({ advanced: [] });`, + `track.applyConstraints({ width: 1280, height: 720 });`, ], invalid: [ @@ -52,6 +53,18 @@ const tests = { code: "import Torch from 'react-native-torch';", errors: [expectedError], }, + { + code: "track.applyConstraints({ advanced: [{ torch: true }] });", + errors: [expectedError], + }, + { + code: "track.applyConstraints({ advanced: [{ torch: false }] });", + errors: [expectedError], + }, + { + code: "track.applyConstraints({ advanced: [{ facingMode: 'environment' }, { torch: true }] });", + errors: [expectedError], + }, ], }; diff --git a/test-project/src/no-torch.js b/test-project/src/no-torch.js index 3e6b55e..98b1d7a 100644 --- a/test-project/src/no-torch.js +++ b/test-project/src/no-torch.js @@ -1,3 +1,16 @@ +// 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(); + + // Non-compliant: programmatically enables torch via advanced constraints + await track.applyConstraints({ advanced: [{ torch: true }] }); + + // Compliant: no torch constraint + await track.applyConstraints({ advanced: [{ facingMode: "environment" }] }); +} diff --git a/test-project/yarn.lock b/test-project/yarn.lock index 4cd76f6..609b97c 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=9e133d&locator=creedengo-javascript-test-project%40workspace%3A." peerDependencies: eslint: ^9.0.0 || ^10.0.0 - checksum: 10c0/35357906578cb26b758f44fb8fdb12656de3a6d3297d97002f3a1c755066b81e1321badd1f4a01a9e9d156b0545b5611774c2ed83e80600168c134f0dc0846fd + checksum: 10c0/9634bfc45fdc5c550fd0345c0621e3d953906a1578ed0340107cacc275fd5616e65bda298a7d43d6c83544cc764b248f267fd58bc9f5683be57695afd283eceb languageName: node linkType: hard From 9186d91d38c10372435eba51905749b96a01e8a6 Mon Sep 17 00:00:00 2001 From: HuMoreau <73228759+HuMoreau@users.noreply.github.com> Date: Wed, 20 May 2026 14:41:59 +0200 Subject: [PATCH 2/6] Fix link formatting for torch property documentation --- eslint-plugin/docs/rules/no-torch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-plugin/docs/rules/no-torch.md b/eslint-plugin/docs/rules/no-torch.md index e698e84..2d92a14 100644 --- a/eslint-plugin/docs/rules/no-torch.md +++ b/eslint-plugin/docs/rules/no-torch.md @@ -35,4 +35,4 @@ await track.applyConstraints({ advanced: [{ facingMode: "environment" }] }); ### 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) +- [MediaTrackConstraints: torch property (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#torch) From 9bb0b924a881bbb5a655299da860348024ecbb06 Mon Sep 17 00:00:00 2001 From: HuMoreau <73228759+HuMoreau@users.noreply.github.com> Date: Wed, 20 May 2026 14:43:13 +0200 Subject: [PATCH 3/6] Update comments for torch constraint compliance --- test-project/src/no-torch.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test-project/src/no-torch.js b/test-project/src/no-torch.js index 98b1d7a..7e4e49e 100644 --- a/test-project/src/no-torch.js +++ b/test-project/src/no-torch.js @@ -8,9 +8,7 @@ export async function example() { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); const [track] = mediaStream.getVideoTracks(); - // Non-compliant: programmatically enables torch via advanced constraints - await track.applyConstraints({ advanced: [{ torch: true }] }); + await track.applyConstraints({ advanced: [{ torch: true }] }); // Non-compliant: programmatically enables torch via advanced constraints - // Compliant: no torch constraint - await track.applyConstraints({ advanced: [{ facingMode: "environment" }] }); + await track.applyConstraints({ advanced: [{ facingMode: "environment" }] }); // Compliant: no torch constraint } From fa2f7e6ee7600657440ee64a9da174ffe1975a9c Mon Sep 17 00:00:00 2001 From: "Hugo.Mo" Date: Wed, 20 May 2026 15:05:57 +0200 Subject: [PATCH 4/6] Restore yarn.lock to upstream state --- test-project/yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-project/yarn.lock b/test-project/yarn.lock index 609b97c..4cd76f6 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=9e133d&locator=creedengo-javascript-test-project%40workspace%3A." + resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=30e64e&locator=creedengo-javascript-test-project%40workspace%3A." peerDependencies: eslint: ^9.0.0 || ^10.0.0 - checksum: 10c0/9634bfc45fdc5c550fd0345c0621e3d953906a1578ed0340107cacc275fd5616e65bda298a7d43d6c83544cc764b248f267fd58bc9f5683be57695afd283eceb + checksum: 10c0/35357906578cb26b758f44fb8fdb12656de3a6d3297d97002f3a1c755066b81e1321badd1f4a01a9e9d156b0545b5611774c2ed83e80600168c134f0dc0846fd languageName: node linkType: hard From bf195f0b272a79925ef0afa40af8d4d2df8c08e2 Mon Sep 17 00:00:00 2001 From: utarwyn Date: Thu, 16 Jul 2026 22:46:01 +0200 Subject: [PATCH 5/6] Enhance no-torch rule --- CHANGELOG.md | 2 +- eslint-plugin/lib/rules/no-torch.js | 71 ++++++++----------- .../tests/lib/rules/no-torch.test.js | 50 ++++++++++++- test-project/src/no-torch.js | 6 +- test-project/yarn.lock | 4 +- 5 files changed, 86 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b969d17..73b4322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- [#48](https://github.com/green-code-initiative/creedengo-javascript/issues/48) Extend rule GCI530 "no-torch" to detect HTML5 Web API usage (`MediaTrackConstraints` torch constraint via `applyConstraints`) +- [#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 diff --git a/eslint-plugin/lib/rules/no-torch.js b/eslint-plugin/lib/rules/no-torch.js index 8c824ca..7d067d1 100644 --- a/eslint-plugin/lib/rules/no-torch.js +++ b/eslint-plugin/lib/rules/no-torch.js @@ -18,45 +18,24 @@ "use strict"; -const reactNativeTorchLibrary = "react-native-torch"; - -function getPropertyName(prop) { - if (prop.key.type === "Identifier") return prop.key.name; - if (prop.key.type === "Literal") return String(prop.key.value); - return null; -} - -function findProperty(objectExpression, name) { - return objectExpression.properties.find( - (p) => p.type === "Property" && getPropertyName(p) === name - ); -} - -function objectHasTorchProperty(objectExpression) { - return Boolean(findProperty(objectExpression, "torch")); -} - -function advancedArrayHasTorch(arrayExpression) { - return arrayExpression.elements.some( - (el) => el && el.type === "ObjectExpression" && objectHasTorchProperty(el) - ); -} +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; +}; -function constraintsArgUsesTorchInAdvanced(arg) { - if (arg.type !== "ObjectExpression") return false; - const advancedProp = findProperty(arg, "advanced"); - if (!advancedProp || advancedProp.value.type !== "ArrayExpression") return false; - return advancedArrayHasTorch(advancedProp.value); -} +const hasTorchTrueInAdvanced = (arg) => { + const advanced = getPropertyValue("advanced")(arg); + if (!advanced || advanced.type !== "ArrayExpression") return false; -function isApplyConstraintsCall(node) { - const { callee } = node; - if (callee.type !== "MemberExpression") return false; - const methodName = callee.computed - ? callee.property.type === "Literal" && callee.property.value - : callee.property.name; - return methodName === "applyConstraints" && node.arguments.length > 0; -} + return advanced.elements.some((el) => { + const torch = getPropertyValue("torch")(el); + return torch?.type === "Literal" && torch.value === true; + }); +}; /** @type {import("eslint").Rule.RuleModule} */ module.exports = { @@ -74,6 +53,8 @@ module.exports = { schema: [], }, create: function (context) { + const reactNativeTorchLibrary = "react-native-torch"; + return { ImportDeclaration(node) { if (node.source.value === reactNativeTorchLibrary) { @@ -85,10 +66,18 @@ module.exports = { }, CallExpression(node) { - if ( - isApplyConstraintsCall(node) && - constraintsArgUsesTorchInAdvanced(node.arguments[0]) - ) { + 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 5f628ea..72a44c5 100644 --- a/eslint-plugin/tests/lib/rules/no-torch.test.js +++ b/eslint-plugin/tests/lib/rules/no-torch.test.js @@ -42,29 +42,75 @@ const expectedError = { const tests = { valid: [ + // 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: "track.applyConstraints({ advanced: [{ torch: true }] });", + code: "import { torch } from 'react-native-torch';", errors: [expectedError], }, { - code: "track.applyConstraints({ advanced: [{ torch: false }] });", + 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/test-project/src/no-torch.js b/test-project/src/no-torch.js index 7e4e49e..f4ad0bf 100644 --- a/test-project/src/no-torch.js +++ b/test-project/src/no-torch.js @@ -5,10 +5,14 @@ Torch.switchState(true); // Web API (MediaTrackConstraints) export async function example() { - const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); + 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 From 3254e07f5d3dda46939502f5e1720da3bbae1d7a Mon Sep 17 00:00:00 2001 From: utarwyn Date: Thu, 16 Jul 2026 23:08:58 +0200 Subject: [PATCH 6/6] Update creedengo rules specifications version to 3.1.0 --- sonar-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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