a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
301 lines
9.9 KiB
Go
301 lines
9.9 KiB
Go
package parser
|
|
|
|
import (
|
|
"bytes"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// sniffPrefixCap bounds how much of a file the content probes scan.
|
|
// Language is overwhelmingly decided by the first few lines, so
|
|
// capping keeps detection cheap on large files.
|
|
const sniffPrefixCap = 64 * 1024
|
|
|
|
// shebangInterpreters maps a script interpreter basename to the Gortex
|
|
// language whose extractor should handle the file. Only interpreters
|
|
// with a registered extractor are listed; DetectLanguageContent still
|
|
// verifies the language is registered before returning it.
|
|
var shebangInterpreters = map[string]string{
|
|
"sh": "bash",
|
|
"bash": "bash",
|
|
"zsh": "bash",
|
|
"ksh": "bash",
|
|
"dash": "bash",
|
|
"ash": "bash",
|
|
"python": "python",
|
|
"python2": "python",
|
|
"python3": "python",
|
|
"perl": "perl",
|
|
"ruby": "ruby",
|
|
"node": "javascript",
|
|
"nodejs": "javascript",
|
|
"deno": "typescript",
|
|
"bun": "typescript",
|
|
"php": "php",
|
|
"lua": "lua",
|
|
"tclsh": "tcl",
|
|
"wish": "tcl",
|
|
"pwsh": "powershell",
|
|
"powershell": "powershell",
|
|
"groovy": "groovy",
|
|
"elixir": "elixir",
|
|
"julia": "julia",
|
|
"rscript": "r",
|
|
}
|
|
|
|
// sniffShebang reads the interpreter from a leading `#!` line and maps
|
|
// it to a language. It handles direct interpreters (`#!/bin/bash`),
|
|
// the `env` indirection (`#!/usr/bin/env python3`, including `env -S`
|
|
// and `env VAR=val interp`), and trailing interpreter version
|
|
// suffixes (`python3.11`). Returns ("", false) when there is no
|
|
// shebang or the interpreter is unrecognised.
|
|
func sniffShebang(content []byte) (string, bool) {
|
|
b := bytes.TrimPrefix(content, []byte{0xEF, 0xBB, 0xBF}) // UTF-8 BOM
|
|
if len(b) < 2 || b[0] != '#' || b[1] != '!' {
|
|
return "", false
|
|
}
|
|
line := b[2:]
|
|
if nl := bytes.IndexByte(line, '\n'); nl >= 0 {
|
|
line = line[:nl]
|
|
}
|
|
fields := strings.Fields(string(line))
|
|
if len(fields) == 0 {
|
|
return "", false
|
|
}
|
|
interp := strings.ToLower(path.Base(fields[0]))
|
|
if interp == "env" {
|
|
// `#!/usr/bin/env [-S] [VAR=val] interp` — the real
|
|
// interpreter is the first token that is neither an env flag
|
|
// nor a NAME=VALUE assignment.
|
|
interp = ""
|
|
for _, f := range fields[1:] {
|
|
if strings.HasPrefix(f, "-") || strings.Contains(f, "=") {
|
|
continue
|
|
}
|
|
interp = strings.ToLower(path.Base(f))
|
|
break
|
|
}
|
|
if interp == "" {
|
|
return "", false
|
|
}
|
|
}
|
|
if lang, ok := shebangInterpreters[interp]; ok {
|
|
return lang, true
|
|
}
|
|
if lang, ok := shebangInterpreters[trimInterpVersion(interp)]; ok {
|
|
return lang, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// trimInterpVersion strips a trailing version suffix from an
|
|
// interpreter name: "python3.11" -> "python", "ruby2" -> "ruby".
|
|
func trimInterpVersion(interp string) string {
|
|
end := len(interp)
|
|
for end > 0 {
|
|
c := interp[end-1]
|
|
if (c >= '0' && c <= '9') || c == '.' {
|
|
end--
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return interp[:end]
|
|
}
|
|
|
|
// sniffAmbiguous refines the language of a file whose extension maps
|
|
// to more than one plausible language. ext is the file extension
|
|
// (with leading dot). It returns (lang, true) when the content
|
|
// clearly indicates a specific language, and ("", false) when the
|
|
// extension is not ambiguous or the probe is inconclusive — the
|
|
// caller then keeps the extension's default mapping.
|
|
func sniffAmbiguous(filePath, ext string, content []byte) (string, bool) {
|
|
if len(content) == 0 {
|
|
return "", false
|
|
}
|
|
probe := content
|
|
if len(probe) > sniffPrefixCap {
|
|
probe = probe[:sniffPrefixCap]
|
|
}
|
|
switch strings.ToLower(ext) {
|
|
case ".json":
|
|
// A Shopify OS 2.0 JSON template / section group (templates/*.json,
|
|
// sections/*.json) wires sections by `type`; every other .json keeps
|
|
// the generic JSON default (package.json, tsconfig.json, …).
|
|
if isShopifyThemeJSONPath(filePath) && hasShopifyTemplateMarkers(probe) {
|
|
return "liquid_json", true
|
|
}
|
|
case ".h":
|
|
// C / C++ / Objective-C header.
|
|
if hasObjCMarkers(probe) {
|
|
return "objc", true
|
|
}
|
|
if hasCppMarkers(probe) {
|
|
return "cpp", true
|
|
}
|
|
case ".inc":
|
|
// A contested extension: assembly, PHP, and Pascal each claim it by
|
|
// default. Reroute to C only when the fragment carries an unmistakable
|
|
// C signal (a C preprocessor directive, or a C block comment alongside
|
|
// braces) and none of the other claimants' markers — so an X-macro /
|
|
// generated-C ".inc" is parsed as C while PHP / Pascal / asm includes
|
|
// keep their default language untouched.
|
|
if hasCFragmentMarkers(probe) && !hasPHPMarkers(probe) && !hasPascalMarkers(probe) {
|
|
return "c", true
|
|
}
|
|
case ".m":
|
|
// Objective-C / MATLAB / Mathematica source.
|
|
if hasObjCMarkers(probe) {
|
|
return "objc", true
|
|
}
|
|
if hasMatlabMarkers(probe) {
|
|
return "matlab", true
|
|
}
|
|
if hasMathematicaMarkers(probe) {
|
|
return "mathematica", true
|
|
}
|
|
case ".xml":
|
|
// A MyBatis mapper / Spring beans XML routes to its specific
|
|
// extractor; every other .xml keeps the generic "xml" default.
|
|
if hasMyBatisMapperMarkers(probe) {
|
|
return "mybatis", true
|
|
}
|
|
if hasSpringBeansMarkers(probe) {
|
|
return "spring", true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// isShopifyThemeJSONPath reports whether a path lives under a Shopify theme's
|
|
// `templates/` or `sections/` directory — the OS 2.0 JSON-template locations.
|
|
func isShopifyThemeJSONPath(filePath string) bool {
|
|
p := "/" + strings.TrimPrefix(strings.ReplaceAll(filePath, "\\", "/"), "/")
|
|
return strings.Contains(p, "/templates/") || strings.Contains(p, "/sections/")
|
|
}
|
|
|
|
// hasShopifyTemplateMarkers reports whether the JSON content carries the OS 2.0
|
|
// section-group shape — a `"sections"` object plus a `"type"` key naming a
|
|
// section file. A cheap byte pre-filter; the extractor parses and validates.
|
|
func hasShopifyTemplateMarkers(b []byte) bool {
|
|
return bytes.Contains(b, []byte(`"sections"`)) && bytes.Contains(b, []byte(`"type"`))
|
|
}
|
|
|
|
// hasMyBatisMapperMarkers reports whether the content is a MyBatis mapper
|
|
// XML document — a `<mapper` root element or the MyBatis mapper DTD. Kept
|
|
// in package parser (inlined rather than calling languages.IsMyBatisMapper)
|
|
// to avoid an import cycle.
|
|
func hasMyBatisMapperMarkers(b []byte) bool {
|
|
return bytes.Contains(b, []byte("<mapper")) || bytes.Contains(b, []byte("mybatis.org//DTD Mapper"))
|
|
}
|
|
|
|
// hasSpringBeansMarkers reports whether the content is a Spring beans XML
|
|
// document — the Spring beans schema namespace, or a `<beans>`/`<bean>`
|
|
// DTD-style document. Inlined in package parser to avoid an import cycle
|
|
// with languages.IsSpringBeansXML.
|
|
func hasSpringBeansMarkers(b []byte) bool {
|
|
return bytes.Contains(b, []byte("springframework.org/schema/beans")) ||
|
|
(bytes.Contains(b, []byte("<beans")) && bytes.Contains(b, []byte("<bean")))
|
|
}
|
|
|
|
// hasObjCMarkers reports whether the content carries a syntax
|
|
// construct unique to Objective-C.
|
|
func hasObjCMarkers(b []byte) bool {
|
|
for _, m := range []string{
|
|
"@interface", "@implementation", "@protocol", "@property", "#import ",
|
|
} {
|
|
if bytes.Contains(b, []byte(m)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasCppMarkers reports whether the content carries a syntax construct
|
|
// that is valid C++ but not C — enough to type an ambiguous `.h`
|
|
// header as C++ rather than C.
|
|
func hasCppMarkers(b []byte) bool {
|
|
for _, m := range []string{
|
|
"::", "namespace ", "template<", "template <",
|
|
"public:", "private:", "protected:", "virtual ",
|
|
"nullptr", "constexpr",
|
|
} {
|
|
if bytes.Contains(b, []byte(m)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasCFragmentMarkers reports whether a contested-extension file carries an
|
|
// unmistakable C signal — a line-leading C preprocessor directive, or a C block
|
|
// comment together with a brace (the shape of a generated initializer / X-macro
|
|
// table). Deliberately conservative so it never steals a PHP / Pascal / asm
|
|
// include.
|
|
func hasCFragmentMarkers(b []byte) bool {
|
|
for _, line := range bytes.Split(b, []byte("\n")) {
|
|
t := bytes.TrimLeft(line, " \t")
|
|
for _, d := range [][]byte{
|
|
[]byte("#include"), []byte("#define"), []byte("#ifndef"),
|
|
[]byte("#ifdef"), []byte("#pragma"), []byte("#undef"), []byte("#if "),
|
|
} {
|
|
if bytes.HasPrefix(t, d) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return bytes.Contains(b, []byte("/*")) && bytes.Contains(b, []byte("{"))
|
|
}
|
|
|
|
// hasPHPMarkers reports whether the content opens a PHP block — the marker that
|
|
// keeps a Drupal `.inc` / `.module` include on the PHP extractor.
|
|
func hasPHPMarkers(b []byte) bool {
|
|
return bytes.Contains(b, []byte("<?php")) || bytes.Contains(b, []byte("<?=")) ||
|
|
bytes.Contains(b, []byte("<?\n")) || bytes.Contains(b, []byte("<? "))
|
|
}
|
|
|
|
// hasPascalMarkers reports whether the content looks like a Pascal / Delphi
|
|
// include — a unit/section keyword or a `{$...}` compiler directive.
|
|
func hasPascalMarkers(b []byte) bool {
|
|
if bytes.Contains(b, []byte("{$")) {
|
|
return true
|
|
}
|
|
for _, line := range bytes.Split(b, []byte("\n")) {
|
|
t := bytes.ToLower(bytes.TrimLeft(line, " \t"))
|
|
for _, kw := range [][]byte{
|
|
[]byte("unit "), []byte("program "), []byte("interface"),
|
|
[]byte("implementation"), []byte("procedure "), []byte("uses "),
|
|
} {
|
|
if bytes.HasPrefix(t, kw) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasMatlabMarkers reports whether the content looks like MATLAB —
|
|
// a `classdef` or a line-leading `function` definition.
|
|
func hasMatlabMarkers(b []byte) bool {
|
|
if bytes.Contains(b, []byte("classdef ")) {
|
|
return true
|
|
}
|
|
for _, line := range bytes.Split(b, []byte("\n")) {
|
|
t := bytes.TrimLeft(line, " \t")
|
|
if bytes.HasPrefix(t, []byte("function ")) || bytes.HasPrefix(t, []byte("function[")) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasMathematicaMarkers reports whether the content looks like a
|
|
// Wolfram Language / Mathematica package. The probes are deliberately
|
|
// conservative — a `BeginPackage[` call or a `(* ::` cell marker —
|
|
// so a C-style `(*ptr)` dereference never trips it.
|
|
func hasMathematicaMarkers(b []byte) bool {
|
|
return bytes.Contains(b, []byte("BeginPackage[")) ||
|
|
bytes.Contains(b, []byte("(* ::")) ||
|
|
bytes.Contains(b, []byte("(*::"))
|
|
}
|