diff --git a/app/lib/language_utils.rb b/app/lib/language_utils.rb index 46a0941a7..ddc1ddbef 100644 --- a/app/lib/language_utils.rb +++ b/app/lib/language_utils.rb @@ -43,7 +43,13 @@ def self.detect_from_extension(entity) end def self.detect_from_entity(entity) = detect_from_filename(entity) || detect_from_extension(entity) - def self.fill_missing_language(raw, entity:) = blank_or_unknown?(raw) ? detect_from_entity(entity) : raw + def self.fill_missing_language(raw, entity:) + if blank_or_unknown?(raw) || File.extname(entity.to_s).downcase == ".asm" + detect_from_entity(entity) || raw + else + raw + end + end # Canonical display name: "js" → "JavaScript", "cpp" → "C++" def self.display_name(raw) = raw.blank? ? "Unknown" : (find_name(raw) || raw) diff --git a/app/services/heartbeat_ingest.rb b/app/services/heartbeat_ingest.rb index 49c68bc3c..34c2fa109 100644 --- a/app/services/heartbeat_ingest.rb +++ b/app/services/heartbeat_ingest.rb @@ -75,10 +75,7 @@ def normalize_direct_heartbeat(heartbeat, last_language:) .where.not(language: [ nil, "", LAST_LANGUAGE_SENTINEL ]).order(time: :desc).pick(:language) end - if attrs[:language].blank? || attrs[:language] == "Unknown" - inferred = LanguageUtils.detect_from_extension(attrs[:entity]) - attrs[:language] = inferred if inferred - end + attrs[:language] = LanguageUtils.fill_missing_language(attrs[:language], entity: attrs[:entity]) attrs[:user_agent] ||= attrs.delete(:plugin) parsed_ua = WakatimeService.parse_user_agent(attrs[:user_agent]) diff --git a/config/languages_custom.yml b/config/languages_custom.yml index e7de2d090..cf3b6922a 100644 --- a/config/languages_custom.yml +++ b/config/languages_custom.yml @@ -11,3 +11,10 @@ Lapse: AsciiDoc: extensions: - ".ad" + +Motorola 68K Assembly: + extensions: + - ".i" + - ".inc" + - ".s" + - ".x68" diff --git a/test/lib/language_utils_test.rb b/test/lib/language_utils_test.rb new file mode 100644 index 000000000..c05992809 --- /dev/null +++ b/test/lib/language_utils_test.rb @@ -0,0 +1,96 @@ +require "test_helper" + +class LanguageUtilsTest < Minitest::Test + def setup + ActiveRecord::FixtureSet.reset_cache + end + + def test_detect_asm_extension_returns_assembly + assert_equal "Assembly", LanguageUtils.detect_from_extension("test.asm") + end + + def test_detect_known_extension + assert_equal "Ruby", LanguageUtils.detect_from_extension("test.rb") + assert_equal "Python", LanguageUtils.detect_from_extension("test.py") + assert_equal "JavaScript", LanguageUtils.detect_from_extension("test.js") + end + + def test_detect_motorola_68k_specific_extensions + assert_equal "Motorola 68K Assembly", LanguageUtils.detect_from_extension("test.x68") + end + + def test_detect_unknown_extension_returns_nil + assert_nil LanguageUtils.detect_from_extension("test.xyzzy") + end + + def test_blank_or_unknown_returns_true_for_blank + assert LanguageUtils.blank_or_unknown?(nil) + assert LanguageUtils.blank_or_unknown?("") + assert LanguageUtils.blank_or_unknown?("Unknown") + assert LanguageUtils.blank_or_unknown?("unknown") + end + + def test_blank_or_unknown_returns_false_for_known + refute LanguageUtils.blank_or_unknown?("Assembly") + refute LanguageUtils.blank_or_unknown?("Ruby") + end + + def test_find_name_resolves_aliases + assert_equal "Assembly", LanguageUtils.find_name("asm") + assert_equal "Assembly", LanguageUtils.find_name("nasm") + assert_equal "Motorola 68K Assembly", LanguageUtils.find_name("m68k") + end + + def test_find_name_returns_nil_for_unknown + assert_nil LanguageUtils.find_name("NotARealLanguage") + end + + def test_display_name_returns_canonical + assert_equal "Assembly", LanguageUtils.display_name("asm") + assert_equal "Assembly", LanguageUtils.display_name("Assembly") + end + + def test_display_name_returns_unknown_for_nil + assert_equal "Unknown", LanguageUtils.display_name(nil) + assert_equal "Unknown", LanguageUtils.display_name("") + end + + def test_fill_missing_language_returns_raw_when_present + assert_equal "Assembly", LanguageUtils.fill_missing_language("Assembly", entity: "test.asm") + end + + def test_fill_missing_language_detects_from_extension_when_blank + assert_equal "Assembly", LanguageUtils.fill_missing_language(nil, entity: "test.asm") + assert_equal "Assembly", LanguageUtils.fill_missing_language("", entity: "test.asm") + assert_equal "Assembly", LanguageUtils.fill_missing_language("Unknown", entity: "test.asm") + end + + def test_fill_missing_language_overrides_asm_variants + assert_equal "Assembly", LanguageUtils.fill_missing_language("RGBDS Assembly", entity: "test.asm") + assert_equal "Assembly", LanguageUtils.fill_missing_language("TASM", entity: "test.asm") + assert_equal "Assembly", LanguageUtils.fill_missing_language("ca65 assembler", entity: "test.asm") + assert_equal "Assembly", LanguageUtils.fill_missing_language("Assembly", entity: "test.asm") + end + + def test_fill_missing_language_does_not_override_non_asm + assert_equal "Python", LanguageUtils.fill_missing_language("Python", entity: "test.py") + assert_equal "RGBDS Assembly", LanguageUtils.fill_missing_language("RGBDS Assembly", entity: "test.py") + end + + def test_color_returns_hex_for_known_language + assert_equal "#6E4C13", LanguageUtils.color("Assembly") + assert_equal "#701516", LanguageUtils.color("Ruby") + end + + def test_color_returns_default_for_unknown + assert_equal LanguageUtils::DEFAULT_COLOR, LanguageUtils.color("NotARealLanguage") + end + + def test_extension_map_overrides_asm_to_assembly + assert_equal "Assembly", LanguageUtils.extension_map[".asm"] + end + + def test_extension_map_still_has_motorola_specific_extensions + assert_equal "Motorola 68K Assembly", LanguageUtils.extension_map[".x68"] + end +end