diff --git a/swift-lang/src/main/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheck.java b/swift-lang/src/main/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheck.java new file mode 100644 index 0000000..7a6f181 --- /dev/null +++ b/swift-lang/src/main/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheck.java @@ -0,0 +1,54 @@ +/* + * ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications + * Copyright © 2023 green-code-initiative (https://www.ecocode.io/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.ecocode.ios.swift.checks.camera; + +import io.ecocode.ios.swift.SwiftRuleCheck; +import io.ecocode.ios.swift.antlr.generated.Swift5Parser; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNodeImpl; +import org.sonar.check.Rule; + +@Rule(key = "EC512") +public class CameraLeakCheck extends SwiftRuleCheck { + private static final String DEFAULT_ISSUE_MESSAGE = "Any started capture session should be stopped."; + private boolean captureSessionStarted = false; + private boolean captureSessionStopped = false; + Swift5Parser.ExpressionContext id; + + @Override + public void apply(ParseTree tree) { + if (tree instanceof Swift5Parser.ExpressionContext && tree.getText().contains("startRunning")) { + id = (Swift5Parser.ExpressionContext) tree; + captureSessionStarted = true; + } + + if (tree instanceof Swift5Parser.ExpressionContext + && (tree.getText().contains("stopRunning"))) { + captureSessionStopped = true; + } + + if (tree instanceof TerminalNodeImpl && tree.getText().equals("")) { + if (captureSessionStarted && !captureSessionStopped) { + this.recordIssue(id.getStart().getStartIndex(), DEFAULT_ISSUE_MESSAGE); + } + captureSessionStarted = false; + captureSessionStopped = false; + } + } +} + diff --git a/swift-lang/src/main/resources/ecocode_swift_profile.json b/swift-lang/src/main/resources/ecocode_swift_profile.json index 8e6eae7..ff79db7 100644 --- a/swift-lang/src/main/resources/ecocode_swift_profile.json +++ b/swift-lang/src/main/resources/ecocode_swift_profile.json @@ -10,6 +10,7 @@ "EC547", "EC523", "EC503", - "EC603" + "EC603", + "EC512" ] } diff --git a/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.html b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.html new file mode 100644 index 0000000..b92861b --- /dev/null +++ b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.html @@ -0,0 +1,44 @@ + +

Most iOS devices come equipped with a variety of sensors that measure motion, orientation, and various environmental + conditions. Additionally, these devices include advanced sensors such as the image sensor (commonly referred to as + the Camera) and the geo-positioning sensor (commonly referred to as GPS). + + The common point of all these sensors is that they are power-intensive while in use. A typical issue arises when + these sensors continue to process data unnecessarily after the application enters an idle state, like when it is + backgrounded or the user stops interacting with it. + + As a result, calls to manage these sensors must be carefully paired: AVCaptureSession.startRunning() and + AVCaptureSession.stopRunning(). Failure to properly manage these calls can lead to significant battery drain within + a few hours.

+

Noncompliant Code Example

+
+import AVFoundation
+
+class CameraManager {
+    var captureSession: AVCaptureSession?
+
+    func activateCamera() {
+        captureSession = AVCaptureSession()
+        captureSession?.startRunning()  // Camera starts capturing
+        // Missing corresponding stopRunning
+    }
+}
+
+

Compliant Code Example

+
+import AVFoundation
+
+class CameraManager {
+    var captureSession: AVCaptureSession?
+
+    func activateCamera() {
+        captureSession = AVCaptureSession()
+        captureSession?.startRunning()  // Camera starts capturing
+    }
+
+    func deactivateCamera() {
+        captureSession?.stopRunning()  // Camera stops capturing
+    }
+}
+
+ diff --git a/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.json b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.json new file mode 100644 index 0000000..0e532c2 --- /dev/null +++ b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC512.json @@ -0,0 +1,18 @@ +{ + "key": "EC512", + "title": "Camera Leakage", + "defaultSeverity": "Major", + "description": "Any started capture session should be stopped.", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "5min" + }, + "tags": [ + "ecocode", + "environment", + "sobriety", + "eco-design" + ], + "type": "CODE_SMELL" +} diff --git a/swift-lang/src/test/java/io/ecocode/ios/swift/EcoCodeSwiftRulesDefinitionTest.java b/swift-lang/src/test/java/io/ecocode/ios/swift/EcoCodeSwiftRulesDefinitionTest.java index 2b88cee..646cdc8 100644 --- a/swift-lang/src/test/java/io/ecocode/ios/swift/EcoCodeSwiftRulesDefinitionTest.java +++ b/swift-lang/src/test/java/io/ecocode/ios/swift/EcoCodeSwiftRulesDefinitionTest.java @@ -54,8 +54,8 @@ public void testMetadata() { } @Test - public void testRegisteredRules() { - assertThat(repository.rules()).hasSize(10); + public void testRegistredRules() { + assertThat(repository.rules()).hasSize(11); } @Test diff --git a/swift-lang/src/test/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheckTest.java b/swift-lang/src/test/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheckTest.java new file mode 100644 index 0000000..2365df6 --- /dev/null +++ b/swift-lang/src/test/java/io/ecocode/ios/swift/checks/camera/CameraLeakCheckTest.java @@ -0,0 +1,51 @@ +/* + * ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications + * Copyright © 2023 green-code-initiative (https://www.ecocode.io/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.ecocode.ios.swift.checks.camera; + +import io.ecocode.ios.swift.checks.CheckTestHelper; +import org.junit.Test; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.batch.sensor.issue.Issue; +import org.sonar.api.batch.sensor.issue.IssueLocation; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CameraLeakCheckTest { + @Test + public void CameraLeak_trigger() { + SensorContextTester context = CheckTestHelper.analyzeTestFile("checks/camera/CameraLeak_trigger.swift"); + assertThat(context.allIssues()).hasSize(1); + Optional issue = context.allIssues().stream().findFirst(); + issue.ifPresent(i -> { + assertThat(i.ruleKey().rule()).isEqualTo("EC512"); + + assertThat(i.ruleKey().repository()).isEqualTo("ecoCode-swift"); + IssueLocation location = i.primaryLocation(); + assertThat(location.textRange().start().line()).isEqualTo(8); + + }); + } + + @Test + public void CameraLeak_no_trigger() { + SensorContextTester context = CheckTestHelper.analyzeTestFile("checks/camera/CameraLeak_no_trigger.swift"); + assertThat(context.allIssues()).isEmpty(); + } +} diff --git a/swift-lang/src/test/resources/checks/camera/CameraLeak_no_trigger.swift b/swift-lang/src/test/resources/checks/camera/CameraLeak_no_trigger.swift new file mode 100644 index 0000000..cd25d08 --- /dev/null +++ b/swift-lang/src/test/resources/checks/camera/CameraLeak_no_trigger.swift @@ -0,0 +1,14 @@ +import AVFoundation + +class CameraManager { + var captureSession: AVCaptureSession? + + func activateCamera() { + captureSession = AVCaptureSession() + captureSession?.startRunning() // Camera starts capturing + } + + func deactivateCamera() { + captureSession?.stopRunning() // Camera stops capturing + } +} \ No newline at end of file diff --git a/swift-lang/src/test/resources/checks/camera/CameraLeak_trigger.swift b/swift-lang/src/test/resources/checks/camera/CameraLeak_trigger.swift new file mode 100644 index 0000000..4778c0b --- /dev/null +++ b/swift-lang/src/test/resources/checks/camera/CameraLeak_trigger.swift @@ -0,0 +1,11 @@ +import AVFoundation + +class CameraManager { + var captureSession: AVCaptureSession? + + func activateCamera() { + captureSession = AVCaptureSession() + captureSession?.startRunning() // Camera starts capturing + // Missing corresponding stopRunning + } +} \ No newline at end of file