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..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..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..7e4e49e 100644 --- a/test-project/src/no-torch.js +++ b/test-project/src/no-torch.js @@ -1,3 +1,14 @@ +// 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: [{ facingMode: "environment" }] }); // Compliant: no torch constraint +}