2020# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222#
23-
23+ import difflib
2424import os
2525import shutil
2626import tempfile
3636from scanpipe .pipes .symbols import SymbolExtractor
3737from scanpipe .pipes .symbols import create_sha256_fingerprint
3838from scanpipe .pipes .symbols import is_supported_language
39- from scanpipe .pipes .unidiff .patch import PatchSet
4039
4140
4241class ReachabilityStatus (str , Enum ):
@@ -149,6 +148,26 @@ def get_commit_diff_text(self):
149148 base = self .parent_commit .hexsha if self .parent_commit else NULL_TREE
150149 return self .repo .git .diff (base , self .commit .hexsha , unified = 3 )
151150
151+ @classmethod
152+ def compute_changed_lines (cls , vulnerable_text , fixed_text ):
153+ """Return the removed and added line numbers between two file contents."""
154+ matcher = difflib .SequenceMatcher (
155+ a = vulnerable_text .splitlines (),
156+ b = fixed_text .splitlines (),
157+ autojunk = False , # otherwise difflib ignores lines that repeat often
158+ )
159+
160+ removed_lines = []
161+ added_lines = []
162+ for tag , vuln_start , vuln_end , fixed_start , fixed_end in matcher .get_opcodes ():
163+ if tag == "equal" :
164+ continue
165+ # opcodes are 0-based and end-exclusive, line numbers are 1-based
166+ removed_lines .extend (range (vuln_start + 1 , vuln_end + 1 ))
167+ added_lines .extend (range (fixed_start + 1 , fixed_end + 1 ))
168+
169+ return removed_lines , added_lines
170+
152171 @classmethod
153172 def diff_changed_symbols (cls , vuln_meta , fixed_meta ):
154173 vuln_only = {
@@ -163,50 +182,16 @@ def diff_changed_symbols(cls, vuln_meta, fixed_meta):
163182 }
164183 return vuln_only , fixed_only
165184
166- @classmethod
167- def parse_diff_lines (cls , diff_text ):
168- diff_map = {}
169- if not diff_text :
170- return diff_map
171-
172- for patched_file in PatchSet .from_string (diff_text ):
173- removed = []
174- added = []
175-
176- for hunk in patched_file :
177- removed .extend (
178- line .source_line_no
179- for line in hunk
180- if line .is_removed and line .source_line_no
181- )
182- added .extend (
183- line .target_line_no
184- for line in hunk
185- if line .is_added and line .target_line_no
186- )
187-
188- candidates = {
189- patched_file .path ,
190- (patched_file .source_file or "" ).removeprefix ("a/" ),
191- (patched_file .target_file or "" ).removeprefix ("b/" ),
192- }
193-
194- for candidate in candidates :
195- if candidate :
196- diff_map [candidate ] = (removed , added )
197-
198- return diff_map
199-
200185 def collect_patch_symbols (self ):
201- diff_text = self . get_commit_diff_text ()
186+ by_language = {}
202187 changed_files = self .get_changed_files ()
203- diff_line_map = self .parse_diff_lines (diff_text = diff_text )
204188
205- by_language = {}
206189 for file_path , texts in changed_files .items ():
207190 vulnerable_text = texts ["vulnerable_text" ]
208191 fixed_text = texts ["fixed_text" ]
209- removed_lines , added_lines = diff_line_map .get (file_path , ([], []))
192+ removed_lines , added_lines = self .compute_changed_lines (
193+ vulnerable_text , fixed_text
194+ )
210195
211196 vuln_meta , fixed_meta , language = self .analyze (
212197 vulnerable_text = vulnerable_text ,
@@ -504,11 +489,9 @@ def build_index(self) -> dict | None:
504489 )
505490
506491 for node , _ in lang_query .get_constants (tree .root_node ):
507- # Skip constants defined inside functions, classes, or other blocks
508- if node .parent == tree .root_node :
509- self .process_node (
510- node , extractor , definitions_index , definitions , fingerprints
511- )
492+ self .process_node (
493+ node , extractor , definitions_index , definitions , fingerprints
494+ )
512495
513496 return {
514497 "definitions" : definitions ,
0 commit comments