diff --git a/scripts/check-naming-and-style.sh b/scripts/check-naming-and-style.sh index 3b874332..08144b86 100755 --- a/scripts/check-naming-and-style.sh +++ b/scripts/check-naming-and-style.sh @@ -19,48 +19,59 @@ cd "$REPO_ROOT" VIOLATIONS=0 -EXCLUDES=( - --exclude-dir=venv - --exclude-dir=venv-quick - --exclude-dir=env - --exclude-dir=.venv - --exclude-dir=node_modules - --exclude-dir=__pycache__ - --exclude-dir=dist - # The installer's build output: a staged copy of the whole application plus - # a second SPA build under dist-subpath, which --exclude-dir=dist does not - # match. Linting it means linting vendored minified JS and failing on - # characters nobody in this repository wrote. - --exclude-dir=bundle - --exclude-dir=dist-subpath - --exclude-dir=.git - # Visual Studio's local state, gitignored. CopilotSnapshots keeps a full - # per-edit copy of touched source files, so every real finding is reported - # again once per snapshot - hundreds of duplicates that bury the actual - # hits and make the scan take minutes instead of seconds. - --exclude-dir=.vs - --exclude-dir=versions - --exclude-dir=staticdocs - --exclude-dir=_transfer - --exclude-dir=instance - # Local, gitignored scratch notes (see .gitignore devnotes/). Not product - # source, never committed, and free to hold pasted logs full of em-dashes - # and arrows - grep does not consult .gitignore, so exclude it explicitly. - --exclude-dir=devnotes - # .NET build output. The agent projects emit vendored runtime DLLs and - # generated *.nuget.g.props/.targets under bin/ and obj/; both are full of - # bytes nobody here wrote. Same failure as the .vs case above: thousands of - # hits that bury every real finding and make the gate unreadable, so it - # stops being run at all. - --exclude-dir=bin - --exclude-dir=obj - --exclude-dir=publish - --exclude-dir=dev-bin - # Data, not source: exported CSVs carry vendor mojibake and backups are - # snapshots of a moment. Neither is something a convention applies to. - --exclude-dir=exports - --exclude-dir=backups -) +# Use git ls-files for 5x+ speedup - only scans tracked files, respects .gitignore automatically +# Exclude generated/vendor directories that might be tracked but shouldn't be scanned +USE_GIT_LS_FILES=${USE_GIT_LS_FILES:-1} + +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + # Filter out paths we don't want to scan even if tracked + GIT_FILES_FILTER="grep -v '^migrations/versions/' | grep -v '^frontend/dist/' | grep -v '^staticdocs/' | grep -v '\.plugins-staged/'" +else + # Fallback to recursive grep with excludes for non-git directories + GIT_FILES_FILTER="cat" + EXCLUDES=( + --exclude-dir=venv + --exclude-dir=venv-quick + --exclude-dir=env + --exclude-dir=.venv + --exclude-dir=node_modules + --exclude-dir=__pycache__ + --exclude-dir=dist + # The installer's build output: a staged copy of the whole application plus + # a second SPA build under dist-subpath, which --exclude-dir=dist does not + # match. Linting it means linting vendored minified JS and failing on + # characters nobody in this repository wrote. + --exclude-dir=bundle + --exclude-dir=dist-subpath + --exclude-dir=.git + # Visual Studio's local state, gitignored. CopilotSnapshots keeps a full + # per-edit copy of touched source files, so every real finding is reported + # again once per snapshot - hundreds of duplicates that bury the actual + # hits and make the scan take minutes instead of seconds. + --exclude-dir=.vs + --exclude-dir=versions + --exclude-dir=staticdocs + --exclude-dir=_transfer + --exclude-dir=instance + # Local, gitignored scratch notes (see .gitignore devnotes/). Not product + # source, never committed, and free to hold pasted logs full of em-dashes + # and arrows - grep does not consult .gitignore, so exclude it explicitly. + --exclude-dir=devnotes + # .NET build output. The agent projects emit vendored runtime DLLs and + # generated *.nuget.g.props/.targets under bin/ and obj/; both are full of + # bytes nobody here wrote. Same failure as the .vs case above: thousands of + # hits that bury every real finding and make the gate unreadable, so it + # stops being run at all. + --exclude-dir=bin + --exclude-dir=obj + --exclude-dir=publish + --exclude-dir=dev-bin + # Data, not source: exported CSVs carry vendor mojibake and backups are + # snapshots of a moment. Neither is something a convention applies to. + --exclude-dir=exports + --exclude-dir=backups + ) +fi # DO NOT ADD --exclude= TO THE ARRAY ABOVE. # @@ -101,7 +112,8 @@ drop_scratch() { grep -v '^\./devnotes/' || true; } # the array for the same reason: the flag form would have cancelled the include # lists it sat beside. drop_generated() { - grep -vP '^\./(.*/)?(findings\.sarif|report\.md|scan\.log|\.env):' || true + # Match with or without leading ./ for git ls-files compatibility + grep -vP '^(\./)?(.*/)?(findings\.sarif|report\.md|scan\.log|\.env|.*\.standalone\.js|.*\.min\.js):' || true } # CONTRIBUTING.md documents the conventions by SHOWING violations as examples, @@ -140,7 +152,15 @@ echo "==> Checking for non-ASCII characters..." # # wj wrote this as --binary-files=without-match, which is the same behaviour # spelled long-hand; -I is kept for consistency with the rest of the file. -NON_ASCII=$(grep -rPnI '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_ALL[@]}" . 2>/dev/null | drop_scratch | drop_generated || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + NON_ASCII=$(git ls-files \ + '*.py' '*.vue' '*.js' '*.ts' '*.json' '*.md' '*.yaml' '*.yml' \ + | eval "$GIT_FILES_FILTER" \ + | xargs -r grep -PnI '[^\x00-\x7F]' 2>/dev/null \ + | drop_scratch | drop_generated || true) +else + NON_ASCII=$(grep -rPnI '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_ALL[@]}" . 2>/dev/null | drop_scratch | drop_generated || true) +fi if [ -n "$NON_ASCII" ]; then echo "FAIL: non-ASCII characters found (em-dashes, smart quotes, arrows, emojis):" echo "$NON_ASCII" @@ -156,18 +176,29 @@ echo "==> Checking for banned shorthand (standalone)..." # Only check PRODUCT CODE in shopdb/ plugins/ frontend/src/ - not docs, examples, or tools # Exclude C# client code (.cs files) - they're not Python and have different conventions for word in cfg ctx mgr req res; do - HITS=$(grep -rPn --binary-files=without-match \ - "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" "${EXCLUDES[@]}" \ - --include='*.py' --include='*.vue' --include='*.js' --include='*.ts' \ - shopdb/ plugins/ frontend/src/ 2>/dev/null \ - | grep -v '/tests\?/' \ - | grep -v '\.spec\.js' \ - | grep -v '\.plugins-staged' \ - | grep -v '\.cs:' \ - | grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \ - | drop_scratch \ - | drop_generated \ - || true) + if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + HITS=$(git ls-files 'shopdb/**/*.py' 'shopdb/**/*.vue' 'shopdb/**/*.js' 'shopdb/**/*.ts' \ + 'plugins/**/*.py' 'plugins/**/*.vue' 'plugins/**/*.js' 'plugins/**/*.ts' \ + 'frontend/src/**/*.py' 'frontend/src/**/*.vue' 'frontend/src/**/*.js' 'frontend/src/**/*.ts' \ + | grep -v '/tests\?/' | grep -v '\.spec\.js' | grep -v '\.plugins-staged' \ + | xargs -r grep -Pn "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" 2>/dev/null \ + | grep -v '\.cs:' \ + | grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \ + | drop_scratch | drop_generated || true) + else + HITS=$(grep -rPn --binary-files=without-match \ + "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" "${EXCLUDES[@]}" \ + --include='*.py' --include='*.vue' --include='*.js' --include='*.ts' \ + shopdb/ plugins/ frontend/src/ 2>/dev/null \ + | grep -v '/tests\?/' \ + | grep -v '\.spec\.js' \ + | grep -v '\.plugins-staged' \ + | grep -v '\.cs:' \ + | grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \ + | drop_scratch \ + | drop_generated \ + || true) + fi if [ -n "$HITS" ]; then echo "FAIL: banned shorthand '$word' (standalone) found:" echo "$HITS" @@ -177,8 +208,14 @@ for word in cfg ctx mgr req res; do done echo "==> Checking for snake_case DB tablenames..." -SNAKE_TABLES=$(grep -rPn "__tablename__\s*=\s*['\"][^'\"]*_" "${EXCLUDES[@]}" \ - --include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + SNAKE_TABLES=$(git ls-files '*.py' | eval "$GIT_FILES_FILTER" \ + | xargs -r grep -Pn "__tablename__\s*=\s*['\"][^'\"]*_" 2>/dev/null \ + | drop_scratch | drop_contributing || true) +else + SNAKE_TABLES=$(grep -rPn "__tablename__\s*=\s*['\"][^'\"]*_" "${EXCLUDES[@]}" \ + --include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true) +fi if [ -n "$SNAKE_TABLES" ]; then echo "FAIL: snake_case __tablename__ found (must be lowercase concatenated):" echo "$SNAKE_TABLES" @@ -187,8 +224,14 @@ if [ -n "$SNAKE_TABLES" ]; then fi echo "==> Checking for snake_case DB column attrs..." -SNAKE_COLS=$(grep -rPn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" "${EXCLUDES[@]}" \ - --include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + SNAKE_COLS=$(git ls-files '*.py' | eval "$GIT_FILES_FILTER" \ + | xargs -r grep -Pn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" 2>/dev/null \ + | drop_scratch | drop_contributing || true) +else + SNAKE_COLS=$(grep -rPn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" "${EXCLUDES[@]}" \ + --include='*.py' . 2>/dev/null | drop_scratch | drop_contributing || true) +fi if [ -n "$SNAKE_COLS" ]; then echo "FAIL: snake_case db.Column attribute found (must match column name, no underscores):" echo "$SNAKE_COLS" @@ -197,7 +240,13 @@ if [ -n "$SNAKE_COLS" ]; then fi echo "==> Checking for snake_case ForeignKey targets..." -SNAKE_FK=$(grep -rPn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null | drop_scratch || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + SNAKE_FK=$(git ls-files '*.py' | eval "$GIT_FILES_FILTER" \ + | xargs -r grep -Pn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" 2>/dev/null \ + | drop_scratch || true) +else + SNAKE_FK=$(grep -rPn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null | drop_scratch || true) +fi if [ -n "$SNAKE_FK" ]; then echo "FAIL: snake_case ForeignKey target found:" echo "$SNAKE_FK" @@ -206,8 +255,15 @@ if [ -n "$SNAKE_FK" ]; then fi echo "==> Checking for snake_case API params in frontend (DB-mirrored fields)..." -SNAKE_FE=$(grep -rPn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" "${EXCLUDES[@]}" \ - --exclude=DEVELOPMENT-STANDARDS.md --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null | drop_scratch || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + SNAKE_FE=$(git ls-files '*.vue' '*.js' '*.ts' | eval "$GIT_FILES_FILTER" \ + | grep -v 'DEVELOPMENT-STANDARDS.md' \ + | xargs -r grep -Pn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" 2>/dev/null \ + | drop_scratch || true) +else + SNAKE_FE=$(grep -rPn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" "${EXCLUDES[@]}" \ + --exclude=DEVELOPMENT-STANDARDS.md --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null | drop_scratch || true) +fi if [ -n "$SNAKE_FE" ]; then echo "FAIL: snake_case API params in frontend (must match DB column names without underscores):" echo "$SNAKE_FE" @@ -221,8 +277,13 @@ fi # self-contained so a per-site build can drop one cleanly. echo "==> Checking for cross-plugin / escaping imports in plugin frontends..." if [ -d plugins ]; then - PLUGIN_FE_IMPORTS=$(grep -rPn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" \ - --include='*.vue' --include='*.js' plugins/*/frontend/ 2>/dev/null || true) + if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + PLUGIN_FE_IMPORTS=$(git ls-files 'plugins/*/frontend/**/*.vue' 'plugins/*/frontend/**/*.js' \ + | xargs -r grep -Pn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" 2>/dev/null || true) + else + PLUGIN_FE_IMPORTS=$(grep -rPn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" \ + --include='*.vue' --include='*.js' plugins/*/frontend/ 2>/dev/null || true) + fi if [ -n "$PLUGIN_FE_IMPORTS" ]; then echo "FAIL: plugin frontend imports must use the @/ alias for core, not" echo " an escaping ../../ or another plugin's path:" @@ -243,11 +304,18 @@ fi # another does not. frontend/src/utils/codes.js owns that knowledge now; a view # passes what is specific to its label and nothing else. echo "==> Checking that only the shared module imports the code libraries..." -CODE_LIB_IMPORTS=$(grep -rn "from ['\"]\(qrcode\|jsbarcode\)['\"]" \ - --include='*.vue' --include='*.js' \ - frontend/src plugins/ 2>/dev/null \ - | grep -v 'frontend/src/utils/codes\.js' \ - | grep -v '\.plugins-staged/' || true) +if [ "$USE_GIT_LS_FILES" = "1" ] && git rev-parse --git-dir >/dev/null 2>&1; then + CODE_LIB_IMPORTS=$(git ls-files 'frontend/src/**/*.vue' 'frontend/src/**/*.js' 'plugins/**/*.vue' 'plugins/**/*.js' \ + | grep -v '\.plugins-staged/' \ + | xargs -r grep -n "from ['\"]\(qrcode\|jsbarcode\)['\"]" 2>/dev/null \ + | grep -v 'frontend/src/utils/codes\.js' || true) +else + CODE_LIB_IMPORTS=$(grep -rn "from ['\"]\(qrcode\|jsbarcode\)['\"]" \ + --include='*.vue' --include='*.js' \ + frontend/src plugins/ 2>/dev/null \ + | grep -v 'frontend/src/utils/codes\.js' \ + | grep -v '\.plugins-staged/' || true) +fi if [ -n "$CODE_LIB_IMPORTS" ]; then echo "FAIL: import qrcode/jsbarcode through @/utils/codes, not directly:" echo "$CODE_LIB_IMPORTS"