Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| About: license, languages, contributors, stars, releases and activity beside the files | 1 | //! Which language each file is written in, and which files count towards a |
| 2 | //! repository's languages: a small version of what people know from the | |
| 3 | //! language bar on code hosts. Programming and markup languages count; | |
| 4 | //! data (JSON, YAML) and prose (Markdown) do not, unless `.gitattributes` | |
| 5 | //! says `linguist-detectable`. Vendored, generated and documentation files | |
| 6 | //! never count, unless `.gitattributes` says otherwise: | |
| 7 | //! | |
| 8 | //! ```text | |
| 9 | //! third_party/** -linguist-vendored | |
| 10 | //! *.gen.ts linguist-generated | |
| 11 | //! docs/** -linguist-documentation | |
| 12 | //! *.inc linguist-language=PHP | |
| 13 | //! *.sql linguist-detectable | |
| 14 | //! ``` | |
| 15 | ||
| 16 | use g1t_contracts::about::LanguageShare; | |
| 17 | ||
| 18 | /// What a language is, as it decides whether it counts by default. | |
| 19 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 20 | pub enum Kind { | |
| 21 | Programming, | |
| 22 | Markup, | |
| 23 | Data, | |
| 24 | Prose, | |
| 25 | } | |
| 26 | ||
| 27 | #[derive(Debug, PartialEq, Eq)] | |
| 28 | pub struct Language { | |
| 29 | pub name: &'static str, | |
| 30 | pub color: Option<&'static str>, | |
| 31 | pub kind: Kind, | |
| 32 | } | |
| 33 | ||
| 34 | const fn lang(name: &'static str, color: &'static str, kind: Kind) -> Language { | |
| 35 | Language { name, color: Some(color), kind } | |
| 36 | } | |
| 37 | ||
| 38 | use Kind::{Data, Markup, Programming, Prose}; | |
| 39 | ||
| 40 | /// Every language g1t knows, with the colors people know them by. | |
| 41 | pub static LANGUAGES: &[Language] = &[ | |
| 42 | lang("Assembly", "#6E4C13", Programming), | |
| 43 | lang("Astro", "#ff5a03", Markup), | |
| 44 | lang("Batchfile", "#C1F12E", Programming), | |
| 45 | lang("Blade", "#f7523f", Markup), | |
| 46 | lang("C", "#555555", Programming), | |
| 47 | lang("C#", "#178600", Programming), | |
| 48 | lang("C++", "#f34b7d", Programming), | |
| 49 | lang("Clojure", "#db5855", Programming), | |
| 50 | lang("CMake", "#DA3434", Programming), | |
| 51 | lang("Common Lisp", "#3fb68b", Programming), | |
| 52 | lang("Crystal", "#000100", Programming), | |
| 53 | lang("CSS", "#663399", Markup), | |
| 54 | lang("Cuda", "#3A4E3A", Programming), | |
| 55 | lang("Dart", "#00B4AB", Programming), | |
| 56 | lang("Dockerfile", "#384d54", Programming), | |
| 57 | lang("EJS", "#a91e50", Markup), | |
| 58 | lang("Elixir", "#6e4a7e", Programming), | |
| 59 | lang("Elm", "#60B5CC", Programming), | |
| 60 | lang("Emacs Lisp", "#c065db", Programming), | |
| 61 | lang("Erlang", "#B83998", Programming), | |
| 62 | lang("F#", "#b845fc", Programming), | |
| 63 | lang("Fortran", "#4d41b1", Programming), | |
| 64 | lang("Gleam", "#ffaff3", Programming), | |
| 65 | lang("GLSL", "#5686a5", Programming), | |
| 66 | lang("Go", "#00ADD8", Programming), | |
| 67 | lang("GraphQL", "#e10098", Data), | |
| 68 | lang("Groovy", "#4298b8", Programming), | |
| 69 | lang("Handlebars", "#f7931e", Markup), | |
| 70 | lang("Haskell", "#5e5086", Programming), | |
| 71 | lang("HCL", "#844FBA", Programming), | |
| 72 | lang("HTML", "#e34c26", Markup), | |
| 73 | lang("Java", "#b07219", Programming), | |
| 74 | lang("JavaScript", "#f1e05a", Programming), | |
| 75 | lang("JSON", "#292929", Data), | |
| 76 | lang("Jsonnet", "#0064bd", Programming), | |
| 77 | lang("Julia", "#a270ba", Programming), | |
| 78 | lang("Jupyter Notebook", "#DA5B0B", Markup), | |
| 79 | lang("Kotlin", "#A97BFF", Programming), | |
| 80 | lang("Less", "#1d365d", Markup), | |
| 81 | lang("Liquid", "#67b8de", Markup), | |
| 82 | lang("Lua", "#000080", Programming), | |
| 83 | lang("Makefile", "#427819", Programming), | |
| 84 | lang("Markdown", "#083fa1", Prose), | |
| 85 | lang("MDX", "#fcb32c", Markup), | |
| 86 | lang("Nim", "#ffc200", Programming), | |
| 87 | lang("Nix", "#7e7eff", Programming), | |
| 88 | lang("Objective-C", "#438eff", Programming), | |
| 89 | lang("Objective-C++", "#6866fb", Programming), | |
| 90 | lang("OCaml", "#ef7a08", Programming), | |
| 91 | lang("Perl", "#0298c3", Programming), | |
| 92 | lang("PHP", "#4F5D95", Programming), | |
| 93 | lang("PLpgSQL", "#336790", Programming), | |
| 94 | lang("PowerShell", "#012456", Programming), | |
| 95 | lang("Pug", "#a86454", Markup), | |
| 96 | lang("Python", "#3572A5", Programming), | |
| 97 | lang("R", "#198CE7", Programming), | |
| 98 | lang("Racket", "#3c5caa", Programming), | |
| 99 | lang("Ruby", "#701516", Programming), | |
| 100 | lang("Rust", "#dea584", Programming), | |
| 101 | lang("Sass", "#a53b70", Markup), | |
| 102 | lang("Scala", "#c22d40", Programming), | |
| 103 | lang("Scheme", "#1e4aff", Programming), | |
| 104 | lang("SCSS", "#c6538c", Markup), | |
| 105 | lang("Shell", "#89e051", Programming), | |
| 106 | lang("Solidity", "#AA6746", Programming), | |
| 107 | lang("SQL", "#e38c00", Data), | |
| 108 | lang("Starlark", "#76d275", Programming), | |
| 109 | lang("Stylus", "#ff6347", Markup), | |
| 110 | lang("Svelte", "#ff3e00", Markup), | |
| 111 | lang("Swift", "#F05138", Programming), | |
| 112 | lang("TOML", "#9c4221", Data), | |
| 113 | lang("TSX", "#3178c6", Programming), | |
| 114 | lang("Twig", "#c1d026", Markup), | |
| 115 | lang("TypeScript", "#3178c6", Programming), | |
| 116 | lang("Vim Script", "#199f4b", Programming), | |
| 117 | lang("Visual Basic .NET", "#945db7", Programming), | |
| 118 | lang("Vue", "#41b883", Markup), | |
| 119 | lang("WebAssembly", "#04133b", Programming), | |
| 120 | lang("XML", "#0060ac", Data), | |
| 121 | lang("YAML", "#cb171e", Data), | |
| 122 | lang("Zig", "#ec915c", Programming), | |
| 123 | ]; | |
| 124 | ||
| 125 | /// A language by name, any case. | |
| 126 | pub fn named(name: &str) -> Option<&'static Language> { | |
| 127 | LANGUAGES.iter().find(|language| language.name.eq_ignore_ascii_case(name)) | |
| 128 | } | |
| 129 | ||
| 130 | /// Files known by their whole name. | |
| 131 | fn by_filename(name: &str) -> Option<&'static str> { | |
| 132 | Some(match name { | |
| 133 | "Dockerfile" | "Containerfile" => "Dockerfile", | |
| 134 | "Makefile" | "GNUmakefile" | "makefile" => "Makefile", | |
| 135 | "CMakeLists.txt" => "CMake", | |
| 136 | "Rakefile" | "Gemfile" | "Vagrantfile" | "Podfile" | "Brewfile" => "Ruby", | |
| 137 | "Jenkinsfile" => "Groovy", | |
| 138 | "BUILD" | "BUILD.bazel" | "WORKSPACE" | "WORKSPACE.bazel" | "Tiltfile" => "Starlark", | |
| 139 | _ if name.starts_with("Dockerfile.") => "Dockerfile", | |
| 140 | _ => return None, | |
| 141 | }) | |
| 142 | } | |
| 143 | ||
| 144 | /// Files known by their extension, lowercased, without the dot. | |
| 145 | fn by_extension(extension: &str) -> Option<&'static str> { | |
| 146 | Some(match extension { | |
| 147 | "asm" | "s" | "nasm" => "Assembly", | |
| 148 | "astro" => "Astro", | |
| 149 | "bat" | "cmd" => "Batchfile", | |
| 150 | "c" | "h" => "C", | |
| 151 | "cs" | "csx" => "C#", | |
| 152 | "cc" | "cpp" | "cxx" | "c++" | "hpp" | "hh" | "hxx" | "h++" | "ipp" | "tpp" => "C++", | |
| 153 | "clj" | "cljs" | "cljc" => "Clojure", | |
| 154 | "cmake" => "CMake", | |
| 155 | "lisp" | "lsp" => "Common Lisp", | |
| 156 | "cr" => "Crystal", | |
| 157 | "css" => "CSS", | |
| 158 | "cu" | "cuh" => "Cuda", | |
| 159 | "dart" => "Dart", | |
| 160 | "dockerfile" => "Dockerfile", | |
| 161 | "ejs" => "EJS", | |
| 162 | "ex" | "exs" => "Elixir", | |
| 163 | "elm" => "Elm", | |
| 164 | "el" => "Emacs Lisp", | |
| 165 | "erl" | "hrl" => "Erlang", | |
| 166 | "fs" | "fsi" | "fsx" => "F#", | |
| 167 | "f" | "f77" | "f90" | "f95" | "f03" | "for" => "Fortran", | |
| 168 | "gleam" => "Gleam", | |
| 169 | "glsl" | "vert" | "frag" | "geom" | "comp" => "GLSL", | |
| 170 | "go" => "Go", | |
| 171 | "graphql" | "gql" | "graphqls" => "GraphQL", | |
| 172 | "groovy" | "gradle" | "gvy" => "Groovy", | |
| 173 | "hbs" | "handlebars" | "mustache" => "Handlebars", | |
| 174 | "hs" | "lhs" => "Haskell", | |
| 175 | "hcl" | "tf" | "tfvars" => "HCL", | |
| 176 | "html" | "htm" | "xhtml" => "HTML", | |
| 177 | "java" => "Java", | |
| 178 | "js" | "mjs" | "cjs" | "jsx" => "JavaScript", | |
| 179 | "json" | "jsonc" | "json5" => "JSON", | |
| 180 | "jsonnet" | "libsonnet" => "Jsonnet", | |
| 181 | "jl" => "Julia", | |
| 182 | "ipynb" => "Jupyter Notebook", | |
| 183 | "kt" | "kts" => "Kotlin", | |
| 184 | "less" => "Less", | |
| 185 | "liquid" => "Liquid", | |
| 186 | "lua" => "Lua", | |
| 187 | "mk" | "mak" => "Makefile", | |
| 188 | "md" | "markdown" | "mdown" => "Markdown", | |
| 189 | "mdx" => "MDX", | |
| 190 | "nim" | "nims" => "Nim", | |
| 191 | "nix" => "Nix", | |
| 192 | "m" => "Objective-C", | |
| 193 | "mm" => "Objective-C++", | |
| 194 | "ml" | "mli" => "OCaml", | |
| 195 | "pl" | "pm" => "Perl", | |
| 196 | "php" | "phtml" => "PHP", | |
| 197 | "pgsql" | "plpgsql" => "PLpgSQL", | |
| 198 | "ps1" | "psm1" | "psd1" => "PowerShell", | |
| 199 | "pug" | "jade" => "Pug", | |
| 200 | "py" | "pyi" | "pyw" => "Python", | |
| 201 | "r" => "R", | |
| 202 | "rkt" => "Racket", | |
| 203 | "rb" | "gemspec" | "rake" => "Ruby", | |
| 204 | "rs" => "Rust", | |
| 205 | "sass" => "Sass", | |
| 206 | "scala" | "sc" => "Scala", | |
| 207 | "scm" | "ss" => "Scheme", | |
| 208 | "scss" => "SCSS", | |
| 209 | "sh" | "bash" | "zsh" | "ksh" => "Shell", | |
| 210 | "sol" => "Solidity", | |
| 211 | "sql" => "SQL", | |
| 212 | "bzl" | "star" => "Starlark", | |
| 213 | "styl" => "Stylus", | |
| 214 | "svelte" => "Svelte", | |
| 215 | "swift" => "Swift", | |
| 216 | "toml" => "TOML", | |
| 217 | "tsx" => "TSX", | |
| 218 | "twig" => "Twig", | |
| 219 | "ts" | "mts" | "cts" => "TypeScript", | |
| 220 | "vim" => "Vim Script", | |
| 221 | "vb" => "Visual Basic .NET", | |
| 222 | "vue" => "Vue", | |
| 223 | "wat" | "wast" => "WebAssembly", | |
| 224 | "xml" | "xsd" | "xsl" | "plist" | "csproj" | "fsproj" | "vbproj" => "XML", | |
| 225 | "yml" | "yaml" => "YAML", | |
| 226 | "zig" => "Zig", | |
| 227 | _ => return None, | |
| 228 | }) | |
| 229 | } | |
| 230 | ||
| 231 | /// The language of a file by its path, if g1t knows it. | |
| 232 | pub fn language_of(path: &str) -> Option<&'static Language> { | |
| 233 | let name = path.rsplit('/').next().unwrap_or(path); | |
| 234 | if name.ends_with(".blade.php") { | |
| 235 | return named("Blade"); | |
| 236 | } | |
| 237 | let found = by_filename(name).or_else(|| { | |
| 238 | let (stem, extension) = name.rsplit_once('.')?; | |
| 239 | if stem.is_empty() { | |
| 240 | return None; | |
| 241 | } | |
| 242 | by_extension(&extension.to_ascii_lowercase()) | |
| 243 | })?; | |
| 244 | named(found) | |
| 245 | } | |
| 246 | ||
| 247 | /// Directories whose code someone else wrote. | |
| 248 | const VENDORED_DIRS: &[&str] = &[ | |
| 249 | "node_modules", | |
| 250 | "vendor", | |
| 251 | "vendors", | |
| 252 | "third_party", | |
| 253 | "third-party", | |
| 254 | "3rdparty", | |
| 255 | "bower_components", | |
| 256 | "jspm_packages", | |
| 257 | "Pods", | |
| 258 | "Carthage", | |
| 259 | "site-packages", | |
| 260 | "venv", | |
| 261 | "__pypackages__", | |
| 262 | ]; | |
| 263 | ||
| 264 | /// Directories that hold docs and examples. | |
| 265 | const DOCUMENTATION_DIRS: &[&str] = &["docs", "doc", "documentation", "examples", "example", "samples"]; | |
| 266 | ||
| 267 | /// Directories of output that a build makes. | |
| 268 | const GENERATED_DIRS: &[&str] = &["dist", "__generated__", "generated", "__pycache__", ".next", ".nuxt", ".svelte-kit"]; | |
| 269 | ||
| 270 | fn directories(path: &str) -> impl Iterator<Item = &str> { | |
| 271 | let mut parts: Vec<&str> = path.split('/').collect(); | |
| 272 | parts.pop(); | |
| 273 | parts.into_iter() | |
| 274 | } | |
| 275 | ||
| 276 | /// Whether a file is someone else's code kept in the repository, or the | |
| 277 | /// configuration under a dot-directory (`.github`, `.vscode`). | |
| 278 | pub fn vendored(path: &str) -> bool { | |
| 279 | let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); | |
| 280 | directories(path).any(|dir| dir.starts_with('.') || VENDORED_DIRS.iter().any(|vendored| dir.eq_ignore_ascii_case(vendored))) | |
| 281 | || name.starts_with("jquery") | |
| 282 | || name.starts_with("bootstrap") && (name.ends_with(".js") || name.ends_with(".css")) | |
| 283 | || matches!(name.as_str(), "gradlew" | "gradlew.bat" | "mvnw" | "mvnw.cmd") | |
| 284 | } | |
| 285 | ||
| 286 | /// Whether a file was made by a tool rather than written: minified, | |
| 287 | /// compiled from a schema, or a lockfile. | |
| 288 | pub fn generated(path: &str) -> bool { | |
| 289 | let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); | |
| 290 | directories(path).any(|dir| GENERATED_DIRS.iter().any(|generated| dir.eq_ignore_ascii_case(generated))) | |
| 291 | || [".min.js", ".min.css", ".min.mjs", ".map", ".pb.go", "_pb2.py", "_pb2_grpc.py", ".pb.cc", ".pb.h", ".g.dart", ".freezed.dart", ".designer.cs", ".lock"] | |
| 292 | .iter() | |
| 293 | .any(|suffix| name.ends_with(suffix)) | |
| 294 | || name.contains(".generated.") | |
| 295 | || matches!(name.as_str(), "package-lock.json" | "pnpm-lock.yaml" | "npm-shrinkwrap.json" | "go.sum" | "composer.lock") | |
| 296 | } | |
| 297 | ||
| 298 | /// Whether a file is documentation or an example. | |
| 299 | pub fn documentation(path: &str) -> bool { | |
| 300 | directories(path).any(|dir| DOCUMENTATION_DIRS.iter().any(|docs| dir.eq_ignore_ascii_case(docs))) | |
| 301 | } | |
| 302 | ||
| 303 | /// What `.gitattributes` says of a path. Each is None when it says nothing. | |
| 304 | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 305 | pub struct Said { | |
| 306 | pub vendored: Option<bool>, | |
| 307 | pub generated: Option<bool>, | |
| 308 | pub documentation: Option<bool>, | |
| 309 | pub detectable: Option<bool>, | |
| 310 | pub language: Option<String>, | |
| 311 | } | |
| 312 | ||
| 313 | /// The `linguist-*` lines of a `.gitattributes` file, in order: a later | |
| 314 | /// line that matches wins, as in git. | |
| 315 | #[derive(Clone, Debug, Default)] | |
| 316 | pub struct Attributes { | |
| 317 | rules: Vec<(String, Said)>, | |
| 318 | } | |
| 319 | ||
| 320 | /// An attribute set (`attr`, `attr=true`), unset (`-attr`, `attr=false`) or | |
| 321 | /// not this one. | |
| 322 | fn switch(word: &str, attribute: &str) -> Option<bool> { | |
| 323 | if let Some(rest) = word.strip_prefix('-') { | |
| 324 | return (rest == attribute).then_some(false); | |
| 325 | } | |
| 326 | if word == attribute { | |
| 327 | return Some(true); | |
| 328 | } | |
| 329 | let (name, value) = word.split_once('=')?; | |
| 330 | (name == attribute).then_some(!matches!(value, "false" | "0")) | |
| 331 | } | |
| 332 | ||
| 333 | impl Attributes { | |
| 334 | pub fn parse(text: &str) -> Self { | |
| 335 | let mut rules = Vec::new(); | |
| 336 | for line in text.lines() { | |
| 337 | let line = line.trim(); | |
| 338 | if line.is_empty() || line.starts_with('#') { | |
| 339 | continue; | |
| 340 | } | |
| 341 | let mut words = line.split_whitespace(); | |
| 342 | let Some(pattern) = words.next() else { continue }; | |
| 343 | let mut said = Said::default(); | |
| 344 | for word in words { | |
| 345 | if let Some(on) = switch(word, "linguist-vendored") { | |
| 346 | said.vendored = Some(on); | |
| 347 | } else if let Some(on) = switch(word, "linguist-generated") { | |
| 348 | said.generated = Some(on); | |
| 349 | } else if let Some(on) = switch(word, "linguist-documentation") { | |
| 350 | said.documentation = Some(on); | |
| 351 | } else if let Some(on) = switch(word, "linguist-detectable") { | |
| 352 | said.detectable = Some(on); | |
| 353 | } else if let Some(name) = word.strip_prefix("linguist-language=") { | |
| 354 | said.language = Some(name.replace("-", " ")); | |
| 355 | } | |
| 356 | } | |
| 357 | if said != Said::default() { | |
| 358 | rules.push((pattern.to_owned(), said)); | |
| 359 | } | |
| 360 | } | |
| 361 | Attributes { rules } | |
| 362 | } | |
| 363 | ||
| 364 | /// What the file's matching lines say, the later over the earlier. | |
| 365 | pub fn of(&self, path: &str) -> Said { | |
| 366 | let mut said = Said::default(); | |
| 367 | for (pattern, rule) in &self.rules { | |
| 368 | if !g1t_rules::glob::path_matches(pattern, path) { | |
| 369 | continue; | |
| 370 | } | |
| 371 | said.vendored = rule.vendored.or(said.vendored); | |
| 372 | said.generated = rule.generated.or(said.generated); | |
| 373 | said.documentation = rule.documentation.or(said.documentation); | |
| 374 | said.detectable = rule.detectable.or(said.detectable); | |
| 375 | if rule.language.is_some() { | |
| 376 | said.language = rule.language.clone(); | |
| 377 | } | |
| 378 | } | |
| 379 | said | |
| 380 | } | |
| 381 | } | |
| 382 | ||
| 383 | /// The language a file counts towards, or None when it does not count. | |
| 384 | pub fn counted(path: &str, attributes: &Attributes) -> Option<&'static Language> { | |
| 385 | let said = attributes.of(path); | |
| 386 | let language = match &said.language { | |
| 387 | Some(name) => named(name).or_else(|| language_of(path))?, | |
| 388 | None => language_of(path)?, | |
| 389 | }; | |
| 390 | if said.vendored.unwrap_or_else(|| vendored(path)) | |
| 391 | || said.generated.unwrap_or_else(|| generated(path)) | |
| 392 | || said.documentation.unwrap_or_else(|| documentation(path)) | |
| 393 | { | |
| 394 | return None; | |
| 395 | } | |
| 396 | let detectable = said.detectable.unwrap_or(matches!(language.kind, Kind::Programming | Kind::Markup)); | |
| 397 | detectable.then_some(language) | |
| 398 | } | |
| 399 | ||
| 400 | /// Each language's share of the bytes, largest first, to one decimal place. | |
| 401 | pub fn shares(bytes: impl IntoIterator<Item = (&'static Language, u64)>) -> Vec<LanguageShare> { | |
| 402 | let mut by_language: Vec<(&'static Language, u64)> = Vec::new(); | |
| 403 | for (language, size) in bytes { | |
| 404 | match by_language.iter_mut().find(|(known, _)| known.name == language.name) { | |
| 405 | Some((_, total)) => *total += size, | |
| 406 | None => by_language.push((language, size)), | |
| 407 | } | |
| 408 | } | |
| 409 | let total: u64 = by_language.iter().map(|(_, size)| size).sum(); | |
| 410 | by_language.retain(|(_, size)| *size > 0); | |
| 411 | by_language.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(b.0.name))); | |
| 412 | by_language | |
| 413 | .into_iter() | |
| 414 | .map(|(language, size)| LanguageShare { | |
| 415 | name: language.name.to_owned(), | |
| 416 | color: language.color.map(str::to_owned), | |
| 417 | bytes: size, | |
| 418 | percent: if total == 0 { 0.0 } else { (size as f64 * 1000.0 / total as f64).round() / 10.0 }, | |
| 419 | }) | |
| 420 | .collect() | |
| 421 | } | |
| 422 | ||
| 423 | #[cfg(test)] | |
| 424 | mod tests { | |
| 425 | use super::*; | |
| 426 | ||
| 427 | fn name(path: &str) -> Option<&'static str> { | |
| 428 | counted(path, &Attributes::default()).map(|language| language.name) | |
| 429 | } | |
| 430 | ||
| 431 | #[test] | |
| 432 | fn files_are_known_by_extension_and_name() { | |
| 433 | assert_eq!(name("src/main.rs"), Some("Rust")); | |
| 434 | assert_eq!(name("app/routes/home.tsx"), Some("TSX")); | |
| 435 | assert_eq!(name("lib/x.mts"), Some("TypeScript")); | |
| 436 | assert_eq!(name("Dockerfile"), Some("Dockerfile")); | |
| 437 | assert_eq!(name("deploy/Dockerfile.prod"), Some("Dockerfile")); | |
| 438 | assert_eq!(name("Makefile"), Some("Makefile")); | |
| 439 | assert_eq!(name("resources/views/home.blade.php"), Some("Blade")); | |
| 440 | assert_eq!(name("SCRIPT.SH"), Some("Shell")); | |
| 441 | assert_eq!(name(".bashrc"), None, "a dotfile has no extension"); | |
| 442 | assert_eq!(name("LICENSE"), None); | |
| 443 | } | |
| 444 | ||
| 445 | #[test] | |
| 446 | fn data_prose_vendored_generated_and_docs_do_not_count() { | |
| 447 | assert_eq!(name("package.json"), None); | |
| 448 | assert_eq!(name("README.md"), None); | |
| 449 | assert_eq!(name("config.yml"), None); | |
| 450 | assert_eq!(name("node_modules/react/index.js"), None); | |
| 451 | assert_eq!(name("web/vendor/lib.go"), None); | |
| 452 | assert_eq!(name("static/jquery-3.7.1.js"), None); | |
| 453 | assert_eq!(name("public/app.min.js"), None); | |
| 454 | assert_eq!(name("dist/index.js"), None); | |
| 455 | assert_eq!(name("api/v1/service.pb.go"), None); | |
| 456 | assert_eq!(name("docs/src/plugin.ts"), None); | |
| 457 | assert_eq!(name("examples/hello.rs"), None); | |
| 458 | assert_eq!(name(".github/scripts/release.sh"), None); | |
| 459 | assert_eq!(name("tests/it.rs"), Some("Rust"), "tests count"); | |
| 460 | } | |
| 461 | ||
| 462 | #[test] | |
| 463 | fn gitattributes_overrides_what_is_counted() { | |
| 464 | let attributes = Attributes::parse( | |
| 465 | "# ours\n\ | |
| 466 | vendor/** -linguist-vendored\n\ | |
| 467 | *.gen.ts linguist-generated=true\n\ | |
| 468 | docs/** linguist-documentation=false\n\ | |
| 469 | *.inc linguist-language=PHP\n\ | |
| 470 | *.sql linguist-detectable\n\ | |
| 471 | src/legacy/*.js -linguist-detectable\n", | |
| 472 | ); | |
| 473 | let of = |path: &str| counted(path, &attributes).map(|language| language.name); | |
| 474 | assert_eq!(of("vendor/ours.go"), Some("Go")); | |
| 475 | assert_eq!(of("src/api.gen.ts"), None); | |
| 476 | assert_eq!(of("docs/site.ts"), Some("TypeScript")); | |
| 477 | assert_eq!(of("lib/header.inc"), Some("PHP")); | |
| 478 | assert_eq!(of("db/schema.sql"), Some("SQL")); | |
| 479 | assert_eq!(of("src/legacy/old.js"), None); | |
| 480 | assert_eq!(of("src/new.js"), Some("JavaScript")); | |
| 481 | } | |
| 482 | ||
| 483 | #[test] | |
| 484 | fn a_later_line_wins() { | |
| 485 | let attributes = Attributes::parse("*.js linguist-vendored\nsrc/*.js -linguist-vendored\n"); | |
| 486 | assert_eq!(attributes.of("src/a.js").vendored, Some(false)); | |
| 487 | assert_eq!(attributes.of("lib/a.js").vendored, Some(true)); | |
| 488 | } | |
| 489 | ||
| 490 | #[test] | |
| 491 | fn shares_add_up_by_language() { | |
| 492 | let rust = named("Rust").unwrap(); | |
| 493 | let ts = named("TypeScript").unwrap(); | |
| 494 | let shares = shares([(rust, 600), (ts, 300), (rust, 100), (ts, 0)]); | |
| 495 | assert_eq!(shares.len(), 2); | |
| 496 | assert_eq!(shares[0].name, "Rust"); | |
| 497 | assert_eq!(shares[0].bytes, 700); | |
| 498 | assert_eq!(shares[0].percent, 70.0); | |
| 499 | assert_eq!(shares[1].percent, 30.0); | |
| 500 | assert_eq!(shares[0].color.as_deref(), Some("#dea584")); | |
| 501 | assert!(super::shares(Vec::new()).is_empty()); | |
| 502 | } | |
| 503 | ||
| 504 | #[test] | |
| 505 | fn every_extension_names_a_known_language() { | |
| 506 | let one_of_each = "asm astro bat c cs cpp clj cmake lisp cr css cu dart dockerfile ejs ex elm el erl fs f90 gleam glsl go graphql \ | |
| 507 | groovy hbs hs tf html java js json jsonnet jl ipynb kt less liquid lua mk md mdx nim nix m mm ml pl php pgsql ps1 pug py r \ | |
| 508 | rkt rb rs sass scala scm scss sh sol sql bzl styl svelte swift toml tsx twig ts vim vb vue wat xml yml zig"; | |
| 509 | for extension in one_of_each.split_whitespace() { | |
| 510 | assert!(language_of(&format!("a.{extension}")).is_some(), "{extension}"); | |
| 511 | } | |
| 512 | for name in ["Dockerfile", "Makefile", "CMakeLists.txt", "Gemfile", "Jenkinsfile", "BUILD.bazel", "x.blade.php"] { | |
| 513 | assert!(language_of(name).is_some(), "{name}"); | |
| 514 | } | |
| 515 | for path in ["a.asm", "a.tsx", "a.ipynb", "a.vue", "a.ex", "a.hcl", "a.sol", "a.zig", "a.wat", "a.bzl", "a.vb", "a.pgsql"] { | |
| 516 | assert!(language_of(path).is_some(), "{path}"); | |
| 517 | } | |
| 518 | let mut names: Vec<&str> = LANGUAGES.iter().map(|language| language.name).collect(); | |
| 519 | names.sort(); | |
| 520 | names.dedup(); | |
| 521 | assert_eq!(names.len(), LANGUAGES.len(), "each language once"); | |
| 522 | } | |
| 523 | } |