#! /usr/bin/env bash
#
# Copy tree-sitter grammars from the semgrep-grammars repo into the current
# language directory.
#
set -eu

name=$(basename "$(pwd)")

# Location of pre-generated grammar.json. This is required
# if we're using a tree-sitter grammar.
#
# It is ignored if 'grammar.js' exists locally.
#
import_from=../semgrep-grammars/lang/"$name"

error() {
  cat >&2 <<EOF
Error: $*
EOF
  exit 1
}

# Returns 0 if the installed tree-sitter version >= $1, 1 otherwise.
ts_version_at_least() {
  tree-sitter --version \
    | sed "s/^tree-sitter \\([0-9.]*\\).*$/$1\\n\\1/" \
    | sort -V -C 2>/dev/null
}

# Determine the ABI to use for all tree-sitter generate calls.
# ABI 15 requires >= 0.25.0 and tree-sitter.json metadata.
# ABI 14 requires >= 0.24.0 (the release that introduced the --abi flag).
# Older versions use --no-bindings instead.
metadata_source=
if [[ -f tree-sitter.json ]]; then
  metadata_source=tree-sitter.json
elif [[ -f "$import_from/tree-sitter.json" ]]; then
  metadata_source="$import_from/tree-sitter.json"
fi

if [[ -n "$metadata_source" ]] && ts_version_at_least 0.25.0; then
  ts_abi=15
elif ts_version_at_least 0.24.0; then
  ts_abi=14
else
  ts_abi=""
fi

metadata_link_created=false
cleanup_metadata() {
  if $metadata_link_created; then
    rm -f tree-sitter.json
  fi
}
trap cleanup_metadata EXIT

if [[ "$ts_abi" == 15 && "$metadata_source" != tree-sitter.json ]]; then
  ln -s "$metadata_source" tree-sitter.json
  metadata_link_created=true
fi

tree_sitter_generate() {
  local abi="$1"
  shift
  if [[ -n "$abi" ]]; then
    tree-sitter generate --abi="$abi" "$@"
  else
    tree-sitter generate --no-bindings "$@"
  fi
}

# Generate C source code for the grammar, in two passes:
#
# 1. Import files from semgrep-grammars
# 2. Simplify grammar.json so it's compatible with ocaml-tree-sitter.
# 3. Generate 'parser.c' from the simplified 'grammar.json'.
#

if [[ -e grammar.js ]]; then
  # This mechanism is used for ocaml-tree-sitter test grammars, which just
  # consist of a local 'grammar.json' file.

  echo "$name: Generating initial 'grammar.json' from 'grammar.js'."
  tree_sitter_generate "$ts_abi"
  mkdir -p src
  if [[ -e orig/LICENSE ]]; then cp -L orig/LICENSE src; fi
  if [[ -e orig/scanner.c ]]; then cp -L orig/scanner.c src; fi
else
  # This mechanism is used with languages whose base grammar is defined
  # in a fetched tree-sitter-* checkout.

  # tree-sitter 0.22.6 creates a dummy 'grammar.js' file if
  # there isn't one already. This creates confusing situations
  # because our input grammar as created by ocaml-tree-sitter is
  # 'src/grammar.json', not the usual 'grammar.js'. Creating our
  # own unparseable 'grammar.js' is a protection against using
  # the wrong grammar.
  # Tracked at https://github.com/tree-sitter/tree-sitter/issues/3415
  echo "..... not the input grammar! Use 'tree-sitter generate src/grammar.json'" \
    > grammar.js

  orig_grammar_json="$import_from"/src/grammar.json
  if [[ ! -e "$orig_grammar_json" ]]; then
    cat >&2 <<EOF
Missing local 'grammar.js' and missing '$orig_grammar_json'.
One of these files must exist.
EOF
    exit 1
  else
    echo "$name: Importing initial 'grammar.json'."
    mkdir -p src
    cp -RL "$import_from"/src/* src/
    rm -f src/parser.c
  fi
fi

echo "$name: Simplifying 'grammar.json' for ocaml-tree-sitter."
mv src/grammar.json src/grammar.json.orig
ocaml-tree-sitter simplify src/grammar.json.orig src/grammar.json

echo "$name: Recovering informational JS grammars from JSON."
ocaml-tree-sitter to-js src/grammar.json.orig grammar-rev.js.orig
ocaml-tree-sitter to-js src/grammar.json grammar-rev.js

echo "$name: Generating definitive 'parser.c'."
tree_sitter_generate "$ts_abi" src/grammar.json

# Make sure we get an error in case of OOM leading to no src/parser.c
test -f src/parser.c || error \
  "tree-sitter didn't generate src/parser.c." \
  "This may happen when not enough memory is available."

# Record the tree-sitter version that produced this parser as a greppable
# provenance comment on the first line, e.g.:
#   // semgrep: generated by tree-sitter 0.26.3 (b40f342...)
# tree-sitter doesn't embed its own semver in generated code (only the ABI,
# via LANGUAGE_VERSION), so we capture it here from the same binary that just
# ran 'generate'. Done unconditionally for every grammar.
# coupling: scripts/sync-grammar-fyi.py reads the '// semgrep: generated by ' prefix back out
# of the parser to populate fyi/tree-sitter-version.
stamp="// semgrep: generated by $(tree-sitter --version)"
{ echo "$stamp"; cat src/parser.c; } > src/parser.c.stamped
mv src/parser.c.stamped src/parser.c
