@cosyte/script-utils 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -118,6 +118,22 @@
118
118
  # * A repo with no such file declares no exclusions.
119
119
  # * The declaration file is itself tracked and is SCANNED LIKE ANY OTHER TRACKED
120
120
  # FILE. It is not self-excluding.
121
+ # * A BOUNDARY is the one entry that names part of a file rather than a whole one:
122
+ #
123
+ # CHANGELOG.md em-only below ## Released before this file was generated
124
+ # CHANGELOG.md unread below ## Released before this file was generated
125
+ #
126
+ # It is for a changelog whose dated archive predates the generated part and is kept
127
+ # verbatim. The path is ONE EXACT TRACKED PATH (no `/` prefix entry, no spaces), and
128
+ # the text after `below ` is a WHOLE LINE of that file, matched exactly. From the
129
+ # first line equal to it to the end of the file is the archive: `em-only` reads the
130
+ # archive for the em dash alone, in every form, so an en dash in an old range passes
131
+ # and an em dash still reds; `unread` does not read the archive at all. Everything
132
+ # above the boundary line is read for both dashes like any other file, and the file's
133
+ # NAME is scanned as always. A boundary line the file does not contain is a stale
134
+ # entry and the run exits 1, so a lost heading fails closed instead of widening what
135
+ # is left out. A path may carry one boundary, and a path the declaration also leaves
136
+ # out whole is refused, since it cannot be both.
121
137
  #
122
138
  # AN ENTRY IS THE LINE VERBATIM: nothing is trimmed off either end, because a path is
123
139
  # bytes and trimming would invent a second spelling for one entry. A line carrying
@@ -255,6 +271,14 @@ PATTERN='(?i)\x{2014}|\x{2013}|&[mn]dash(?![a-z])|&#0*821[12](?![0-9])|&#x0*201[
255
271
  PATTERN+='|%E2%80%9[34]|\\u(?:0000)?201[34]|\\u\{0*201[34]\}|\\x\{0*201[34]\}'
256
272
  PATTERN+='|\\0{0,2}201[34](?![0-9a-f])|\\N\{E[MN] DASH\}'
257
273
 
274
+ # The em dash's half of PATTERN, form for form, for the archive a declared `em-only`
275
+ # boundary reads. The self-test below holds it to matching the em dash and NOT the en
276
+ # dash, and test/check-no-emdash.test.ts holds every em form to it, so it cannot drift
277
+ # from PATTERN without a red.
278
+ EM_PATTERN='(?i)\x{2014}|&mdash(?![a-z])|&#0*8212(?![0-9])|&#x0*2014(?![0-9a-f])'
279
+ EM_PATTERN+='|%E2%80%94|\\u(?:0000)?2014|\\u\{0*2014\}|\\x\{0*2014\}'
280
+ EM_PATTERN+='|\\0{0,2}2014(?![0-9a-f])|\\N\{EM DASH\}'
281
+
258
282
  # The declaration's path, relative to the top level of the repo under scan. Deliberately
259
283
  # NOT relative to this file: in a consuming repo this file lives under `node_modules/`
260
284
  # and the declaration is the consumer's own tracked file.
@@ -314,6 +338,25 @@ if [ "$#" -gt 0 ] && [ "$1" = '--' ]; then
314
338
  shift
315
339
  fi
316
340
 
341
+ usage() {
342
+ cat << 'USAGE'
343
+ check-no-emdash: refuse the em dash (U+2014) and the en dash (U+2013), as the characters and
344
+ in every encoded form, in the tracked files of the repository it runs in and in their names.
345
+
346
+ check-no-emdash.sh scan every tracked file and every tracked file name
347
+ check-no-emdash.sh --stdin LABEL scan the text on stdin (a PR title, body, commit message)
348
+ check-no-emdash.sh --list-scanned write the NUL-separated paths the default scan reads
349
+ check-no-emdash.sh --self-id write the sha256 of the implementation that is executing
350
+
351
+ A repository leaves paths out in scripts/check-no-emdash.exclude: one per line, an entry ending
352
+ in / covers everything beneath it, # starts a comment. A changelog archive is named by a boundary,
353
+ `PATH em-only below LINE` or `PATH unread below LINE`: from the first line of PATH equal to LINE to
354
+ the end, em-only reads for the em dash alone and unread reads nothing; above it, both dashes as
355
+ usual. Exit 0 means the mode completed and found nothing banned; exit 1 means anything else. Under
356
+ CI, --help exits 1: it checks nothing.
357
+ USAGE
358
+ }
359
+
317
360
  case "${1:-}" in
318
361
  '')
319
362
  MODE=files
@@ -339,19 +382,17 @@ case "${1:-}" in
339
382
  fi
340
383
  ;;
341
384
  --help | -h)
342
- cat << 'USAGE'
343
- check-no-emdash: refuse the em dash (U+2014) and the en dash (U+2013), as the characters and
344
- in every encoded form, in the tracked files of the repository it runs in and in their names.
345
-
346
- check-no-emdash.sh scan every tracked file and every tracked file name
347
- check-no-emdash.sh --stdin LABEL scan the text on stdin (a PR title, body, commit message)
348
- check-no-emdash.sh --list-scanned write the NUL-separated paths the default scan reads
349
- check-no-emdash.sh --self-id write the sha256 of the implementation that is executing
350
-
351
- A repository leaves paths out in scripts/check-no-emdash.exclude: one per line, an entry ending
352
- in / covers everything beneath it, # starts a comment. Exit 0 means the mode completed and found
353
- nothing banned; exit 1 means anything else.
354
- USAGE
385
+ # Under CI the help is a refusal: it prints this text and checks nothing, so a CI step that
386
+ # runs it must not pass. `CI` set to anything but empty, `false` or `0` is how GitHub Actions
387
+ # and the other common runners announce themselves.
388
+ if [ -n "${CI:-}" ] && [ "${CI}" != false ] && [ "${CI}" != 0 ]; then
389
+ usage >&2
390
+ echo "ERROR: check-no-emdash - refusing --help under CI (CI=${CI}). It prints this text and" >&2
391
+ echo " checks nothing, so a CI step that runs it must not pass. Run the gate without" >&2
392
+ echo " --help." >&2
393
+ exit 1
394
+ fi
395
+ usage
355
396
  exit 0
356
397
  ;;
357
398
  *)
@@ -557,6 +598,13 @@ entry_matches_a_tracked_path() {
557
598
 
558
599
  # ---- the exclusion declaration ---------------------------------------------------
559
600
  EXCLUDES=()
601
+ # A declared boundary, one index per entry: the path, `em-only` or `unread`, the boundary
602
+ # line as written, and the number of the first line of the file equal to it.
603
+ BOUND_PATH=()
604
+ BOUND_MODE=()
605
+ BOUND_TEXT=()
606
+ BOUND_LINE=()
607
+ BOUNDARY_SHAPE='^([^ ]+) (em-only|unread) below (.+)$'
560
608
  refuse_declaration() {
561
609
  local line="$1" entry="$2" what="$3"
562
610
  echo "ERROR: check-no-emdash - unusable entry in ${DECLARATION}, line ${line}: ${entry}" >&2
@@ -589,6 +637,45 @@ if is_tracked "$DECLARATION"; then
589
637
  # The entry is the line VERBATIM. See the header: nothing is trimmed, because a path
590
638
  # is bytes. The anchor rules are the whole of what makes an entry usable on its face.
591
639
  entry=$line
640
+
641
+ # A BOUNDARY names part of one file. It is recognised by its shape and validated on its
642
+ # own terms, so nothing below it can mistake it for a path with spaces in it.
643
+ if [[ $entry =~ $BOUNDARY_SHAPE ]]; then
644
+ b_path=${BASH_REMATCH[1]}
645
+ b_mode=${BASH_REMATCH[2]}
646
+ b_text=${BASH_REMATCH[3]}
647
+ case $b_path in
648
+ /* | ./* | *..* | */)
649
+ refuse_declaration "$lineno" "$entry" \
650
+ "A boundary names ONE tracked file, anchored at the repository root: not absolute, not ./, no .., and not a directory."
651
+ ;;
652
+ esac
653
+ if ! is_tracked "$b_path"; then
654
+ refuse_declaration "$lineno" "$entry" \
655
+ "No tracked file is spelled ${b_path}. A boundary that has outlived its file is a hole nobody is looking at."
656
+ fi
657
+ for i in "${!BOUND_PATH[@]}"; do
658
+ if [ "${BOUND_PATH[$i]}" = "$b_path" ]; then
659
+ refuse_declaration "$lineno" "$entry" \
660
+ "${b_path} already carries a boundary. A file has one archive, so it has one boundary."
661
+ fi
662
+ done
663
+ if [ ! -f "$b_path" ] || [ ! -r "$b_path" ]; then
664
+ refuse_declaration "$lineno" "$entry" \
665
+ "${b_path} is not a readable regular file, so its boundary line cannot be looked for."
666
+ fi
667
+ b_line=$(grep -n -m1 -xF -e "$b_text" -- "$b_path" 2> /dev/null || true)
668
+ b_line=${b_line%%:*}
669
+ if [ -z "$b_line" ]; then
670
+ refuse_declaration "$lineno" "$entry" \
671
+ "${b_path} has no line that is exactly: ${b_text}. A lost boundary fails closed rather than leaving out more than was declared: restore the line, or correct or delete the entry."
672
+ fi
673
+ BOUND_PATH+=("$b_path")
674
+ BOUND_MODE+=("$b_mode")
675
+ BOUND_TEXT+=("$b_text")
676
+ BOUND_LINE+=("$b_line")
677
+ continue
678
+ fi
592
679
  case $entry in
593
680
  /*)
594
681
  refuse_declaration "$lineno" "$entry" \
@@ -648,6 +735,30 @@ is_excluded() {
648
735
  return 1
649
736
  }
650
737
 
738
+ # A path is either left out whole or read up to its boundary. Both at once would leave the
739
+ # reader unable to say which one the gate honoured, so it is refused, whichever line came
740
+ # first.
741
+ for i in "${!BOUND_PATH[@]}"; do
742
+ if is_excluded "${BOUND_PATH[$i]}"; then
743
+ echo "ERROR: check-no-emdash - ${DECLARATION} both leaves out ${BOUND_PATH[$i]} and" >&2
744
+ echo " declares a boundary in it. A file is either left out whole or read up to its" >&2
745
+ echo " boundary; delete one of the two entries." >&2
746
+ exit 1
747
+ fi
748
+ done
749
+
750
+ # The index of the boundary declared for a path, or nothing.
751
+ boundary_index() {
752
+ local p="$1" i
753
+ for i in "${!BOUND_PATH[@]}"; do
754
+ if [ "${BOUND_PATH[$i]}" = "$p" ]; then
755
+ printf '%s' "$i"
756
+ return 0
757
+ fi
758
+ done
759
+ return 1
760
+ }
761
+
651
762
  # ---- --self-id: which bytes am I ------------------------------------------------
652
763
  #
653
764
  # Answered before the scanner assertions, because this mode reads no scanned content and
@@ -736,6 +847,20 @@ if [ "$MODE" = files ] || [ "$MODE" = stdin ]; then
736
847
  echo " Refusing to report findings from a scanner that flags what the rule allows." >&2
737
848
  exit 1
738
849
  fi
850
+ # The archive reading a declared `em-only` boundary uses: it must still see the em dash,
851
+ # and it must NOT see the en dash, or `em-only` would mean something other than it says.
852
+ if [ "${#BOUND_PATH[@]}" -gt 0 ]; then
853
+ if ! printf 'a\xe2\x80\x94b\n' | grep -qP "$EM_PATTERN"; then
854
+ echo "ERROR: check-no-emdash - the archive scanner cannot match a known em dash." >&2
855
+ echo " Refusing to read a declared boundary's archive with a scanner that cannot see." >&2
856
+ exit 1
857
+ fi
858
+ if printf 'a\xe2\x80\x93b\n' | grep -qP "$EM_PATTERN"; then
859
+ echo "ERROR: check-no-emdash - the archive scanner matched an en dash, so it is not reading" >&2
860
+ echo " for the em dash alone. Refusing to report what an em-only boundary allows." >&2
861
+ exit 1
862
+ fi
863
+ fi
739
864
 
740
865
  # SCANNER VISIBILITY PROBE: prove the scanner can still READ a file before believing a
741
866
  # word it says about one. The self-test above asserts what the tool MATCHES; this
@@ -901,12 +1026,20 @@ for i in "${!TRACKED[@]}"; do
901
1026
  exit 1
902
1027
  fi
903
1028
 
1029
+ # A file with a declared boundary is read on its own below, in two halves. It is still
1030
+ # READ, so it is listed for --list-scanned and counted as scanned.
1031
+ if boundary_index "$f" > /dev/null; then
1032
+ printf '%s\0' "$f" >> "$PLAINLIST"
1033
+ scanned=$((scanned + 1))
1034
+ continue
1035
+ fi
1036
+
904
1037
  printf './%s\0' "$f" >> "$SCANLIST"
905
1038
  printf '%s\0' "$f" >> "$PLAINLIST"
906
1039
  scanned=$((scanned + 1))
907
1040
  done
908
1041
 
909
- if [ ! -s "$SCANLIST" ]; then
1042
+ if [ ! -s "$SCANLIST" ] && [ "${#BOUND_PATH[@]}" -eq 0 ]; then
910
1043
  echo "ERROR: check-no-emdash - no tracked files left to scan. Refusing to report" >&2
911
1044
  echo " green from a scan that read nothing." >&2
912
1045
  exit 1
@@ -932,6 +1065,38 @@ HITS=$(xargs -0 -r grep -H -nP -e "$PATTERN" -- < "$SCANLIST" 2>> "$ERRLOG" || t
932
1065
 
933
1066
  refuse_if_incomplete
934
1067
 
1068
+ # The files with a declared boundary. Every hit is found with the full pattern first, so
1069
+ # the line numbers are the file's own; a hit above the boundary line stands, and a hit at
1070
+ # or below it stands only for `em-only` and only when the line holds an em dash.
1071
+ bounded_note=''
1072
+ for i in "${!BOUND_PATH[@]}"; do
1073
+ b_path=${BOUND_PATH[$i]}
1074
+ b_line=${BOUND_LINE[$i]}
1075
+ b_mode=${BOUND_MODE[$i]}
1076
+ b_hits=$(grep -H -nP -e "$PATTERN" -- "./${b_path}" 2>> "$ERRLOG" || true)
1077
+ refuse_if_incomplete
1078
+ while IFS= read -r hit; do
1079
+ [ -n "$hit" ] || continue
1080
+ n=${hit#"./${b_path}:"}
1081
+ n=${n%%:*}
1082
+ keep=0
1083
+ if [ "$n" -lt "$b_line" ]; then
1084
+ keep=1
1085
+ elif [ "$b_mode" = em-only ] && printf '%s\n' "${hit#"./${b_path}:${n}:"}" | grep -qP -e "$EM_PATTERN"; then
1086
+ keep=1
1087
+ fi
1088
+ if [ "$keep" -eq 1 ]; then
1089
+ HITS="${HITS:+${HITS}
1090
+ }${hit}"
1091
+ fi
1092
+ done <<< "$b_hits"
1093
+ if [ "$b_mode" = em-only ]; then
1094
+ bounded_note="${bounded_note}, ${b_path} from line ${b_line} read for the em dash only"
1095
+ else
1096
+ bounded_note="${bounded_note}, ${b_path} from line ${b_line} not read"
1097
+ fi
1098
+ done
1099
+
935
1100
  if [ -n "$NAME_HITS" ] && [ -n "$HITS" ]; then
936
1101
  fail_with_hits "the tracked file names and the tracked files listed above" \
937
1102
  "$NAME_HITS"$'\n'"$HITS"
@@ -941,4 +1106,4 @@ fi
941
1106
 
942
1107
  echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s)" \
943
1108
  "or their names; ${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the" \
944
- "implementation under scan, ${declared_excluded} excluded by ${DECLARATION})"
1109
+ "implementation under scan, ${declared_excluded} excluded by ${DECLARATION}${bounded_note})"
@@ -68,6 +68,16 @@ export interface SourceDocCommentsAxis {
68
68
  extraRules?: ExtraRule[];
69
69
  }
70
70
 
71
+ /** Whether the source string-literal pass runs, over what, and with which extra rules. */
72
+ export interface SourceStringLiteralsAxis {
73
+ /** @default false */
74
+ enabled: boolean;
75
+ /** Tracked pathspecs holding the source whose string literals reach a consumer as messages. */
76
+ paths?: string[];
77
+ /** Detection that applies to string literals only. */
78
+ extraRules?: ExtraRule[];
79
+ }
80
+
71
81
  export interface InternalRefsConfig {
72
82
  /**
73
83
  * AXIS 1, REQUIRED. The project and programme prefixes an identifier is keyed on, and an empty
@@ -113,6 +123,15 @@ export interface InternalRefsConfig {
113
123
  */
114
124
  sourceDocComments?: SourceDocCommentsAxis;
115
125
 
126
+ /**
127
+ * DEFAULTED OFF. The source string-literal pass: double-quoted, single-quoted and template
128
+ * literals, decoded, with every comment skipped, because a string literal reaches a consumer as
129
+ * error and warning text and a comment does not. A NUL byte in source reads as a space.
130
+ *
131
+ * @default { enabled: false }
132
+ */
133
+ sourceStringLiterals?: SourceStringLiteralsAxis;
134
+
116
135
  /**
117
136
  * AXIS 6, DEFAULTED. `clean` must be 0 and the other two must not be, which is checked: a mapping
118
137
  * that put `hits` on 0 would turn every finding into a green run.
@@ -207,6 +226,18 @@ export declare function runSelfTests(rules: CompiledRule[]): string[];
207
226
  /** Join a document the way markdown renders it, so a violation that straddles a wrap is visible. */
208
227
  export declare function reflowParagraphs(text: string): string[];
209
228
 
229
+ /**
230
+ * Extract the string literals of one JavaScript or TypeScript source file, decoded, with every
231
+ * comment skipped: one entry per literal, and one per source line holding two or more of them.
232
+ */
233
+ export declare function extractStringLiterals(
234
+ text: string,
235
+ file: string,
236
+ ): {
237
+ literals: { text: string; where: string }[];
238
+ lines: { text: string; where: string }[];
239
+ };
240
+
210
241
  /** Extract doc-comment text from one source file, with each line's location beside it. */
211
242
  export declare function extractDocComments(
212
243
  text: string,