diff --git a/swift-lang/src/main/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheck.java b/swift-lang/src/main/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheck.java new file mode 100644 index 0000000..c067b64 --- /dev/null +++ b/swift-lang/src/main/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheck.java @@ -0,0 +1,65 @@ +/* + * 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.sobriety; + +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 = "EC515") +public class AudioRecorderLeakCheck extends SwiftRuleCheck { + private static final String DEFAULT_ISSUE_MESSAGE = "Any audio recording started should be stopped."; + Swift5Parser.ExpressionContext id = null; + private boolean audioRecorderStarted = false; + private boolean audioRecorderStopped = false; + private boolean importExist = false; + + + @Override + public void apply(ParseTree tree) { + if (!importExist && tree instanceof Swift5Parser.ExpressionContext && tree.getText().contains("AVAudioRecorder")) { + importExist = true; + } + + if (importExist) { + findStartedButNotStoppedAudioRecord(tree); + } + } + + private void findStartedButNotStoppedAudioRecord(ParseTree tree) { + if (tree instanceof Swift5Parser.ExpressionContext && tree.getText().contains("record()")) { + id = (Swift5Parser.ExpressionContext) tree; + audioRecorderStarted = true; + } + + if (tree instanceof Swift5Parser.ExpressionContext && (tree.getText().contains("stop()"))) { + audioRecorderStopped = true; + } + + if (tree instanceof TerminalNodeImpl && tree.getText().equals("")) { + if (audioRecorderStarted && !audioRecorderStopped) { + this.recordIssue(id.getStart().getStartIndex(), DEFAULT_ISSUE_MESSAGE); + } + audioRecorderStarted = false; + audioRecorderStopped = false; + importExist = false; + } + } +} \ No newline at end of file diff --git a/swift-lang/src/main/resources/ecocode_swift_profile.json b/swift-lang/src/main/resources/ecocode_swift_profile.json index f9c9f0d..ef3480d 100644 --- a/swift-lang/src/main/resources/ecocode_swift_profile.json +++ b/swift-lang/src/main/resources/ecocode_swift_profile.json @@ -6,6 +6,7 @@ "EC512", "EC513", "EC514", + "EC515", "EC519", "EC520", "EC522", diff --git a/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.html b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.html new file mode 100644 index 0000000..0994c7b --- /dev/null +++ b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.html @@ -0,0 +1,58 @@ +

Creation of an AVAudioRecorder object is used to record audio. This class has methods to stop recording + and release resources. + + In addition to unnecessary resources (such as memory and instances of codecs) being held, failure to properly stop + and release these objects if they are no longer needed may also lead to continuous battery consumption for mobile + devices.

+

Noncompliant Code Example

+
+import AVFoundation
+
+var audioRecorder: AVAudioRecorder?
+
+func startRecording() {
+    let settings = [
+        AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+        AVSampleRateKey: 12000,
+        AVNumberOfChannelsKey: 1,
+        AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
+    ]
+
+    do {
+        audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
+        audioRecorder?.record()
+    } catch {
+        // Handle error
+    }
+}
+
+
+

Compliant Code Example

+
+import AVFoundation
+
+var audioRecorder: AVAudioRecorder?
+
+func startRecording() {
+    let settings = [
+        AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+        AVSampleRateKey: 12000,
+        AVNumberOfChannelsKey: 1,
+        AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
+    ]
+
+    do {
+        audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings)
+        audioRecorder?.record()
+    } catch {
+        // Handle error
+    }
+}
+
+func stopRecording() {
+    if let recorder = audioRecorder, recorder.isRecording {
+        recorder.stop()
+        audioRecorder = nil
+    }
+}
+
\ No newline at end of file diff --git a/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.json b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.json new file mode 100644 index 0000000..ec95af4 --- /dev/null +++ b/swift-lang/src/main/resources/io/ecocode/rules/swift/EC515.json @@ -0,0 +1,19 @@ +{ + "key": "EC515", + "title": "Leakage: Audio and Video Recorder Leak", + "defaultSeverity": "Major", + "description": "Creation of an AVAudioRecorder object is used to record audio. This class has methods to stop recording and release resources. In addition to unnecessary resources (such as memory and instances of codecs) being held, failure to properly stop and release these objects if they are no longer needed may also lead to continuous battery consumption for mobile devices.", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "7min" + }, + "tags": [ + "sobriety", + "environment", + "ecocode", + "eco-design" + ], + "type": "CODE_SMELL" + } + \ No newline at end of file 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 6d51e57..8e3f299 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 @@ -55,7 +55,7 @@ public void testMetadata() { @Test public void testRegisteredRules() { - assertThat(repository.rules()).hasSize(13); + assertThat(repository.rules()).hasSize(14); } @Test diff --git a/swift-lang/src/test/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheckTest.java b/swift-lang/src/test/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheckTest.java new file mode 100644 index 0000000..c311c75 --- /dev/null +++ b/swift-lang/src/test/java/io/ecocode/ios/swift/checks/sobriety/AudioRecorderLeakCheckTest.java @@ -0,0 +1,55 @@ +/* + * 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.sobriety; + +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 AudioRecorderLeakCheckTest { + private static final String TEST_AUDIO_CASE_MISSING_RELEASE_CALL = "checks/sobriety/AudioRecorderLeak_trigger.swift"; + private static final String TEST_AUDIO_CASE_COMPLIANT = "checks/sobriety/AudioRecorderLeak_no_trigger.swift"; + + private static final String TESTED_RULE_ID = "EC515"; + private static final String TEST_REPOSITORY = "ecoCode-swift"; + + @Test + public void audioLeakCheck_missing_release_trigger() { + SensorContextTester context = CheckTestHelper.analyzeTestFile(TEST_AUDIO_CASE_MISSING_RELEASE_CALL); + assertThat(context.allIssues()).hasSize(1); + Optional issue = context.allIssues().stream().findFirst(); + issue.ifPresent(i -> { + assertThat(i.ruleKey().rule()).isEqualTo(TESTED_RULE_ID); + assertThat(i.ruleKey().repository()).isEqualTo(TEST_REPOSITORY); + IssueLocation location = i.primaryLocation(); + assertThat(location.textRange().start().line()).isEqualTo(15); + }); + } + + @Test + public void audioLeakCheck_no_trigger() { + SensorContextTester context = CheckTestHelper.analyzeTestFile(TEST_AUDIO_CASE_COMPLIANT); + assertThat(context.allIssues()).isEmpty(); + } +} \ No newline at end of file diff --git a/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_no_trigger.swift b/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_no_trigger.swift new file mode 100644 index 0000000..82bc749 --- /dev/null +++ b/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_no_trigger.swift @@ -0,0 +1,26 @@ +import AVFoundation + +var audioRecorder: AVAudioRecorder? + +func startRecording() { + let settings = [ + AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue + ] + + do { + audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings) + audioRecorder?.record() + } catch { + // Handle error + } +} + +func stopRecording() { + if let recorder = audioRecorder, recorder.isRecording { + recorder.stop() + audioRecorder = nil + } +} \ No newline at end of file diff --git a/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_trigger.swift b/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_trigger.swift new file mode 100644 index 0000000..87a5c8c --- /dev/null +++ b/swift-lang/src/test/resources/checks/sobriety/AudioRecorderLeak_trigger.swift @@ -0,0 +1,19 @@ +import AVFoundation + +var audioRecorder: AVAudioRecorder? + +func startRecording() { + let settings = [ + AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue + ] + + do { + audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: settings) + audioRecorder?.record() + } catch { + // Handle error + } +} \ No newline at end of file