Skip to content

Commit 61b4e88

Browse files
committed
Improve sidecare to handle ORM like sqlalchemy
1 parent 376947c commit 61b4e88

File tree

1 file changed

+124
-35
lines changed

1 file changed

+124
-35
lines changed

resources/templates/sidecar/common/run_sidecar.sh

Lines changed: 124 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,54 @@ _filter_crosshair_dirs() {
148148
echo "${filtered[@]}"
149149
}
150150

151+
# Convert source directory paths to Python module names for CrossHair
152+
# CrossHair expects module names (e.g., "sqlalchemy") not paths (e.g., "lib/sqlalchemy")
153+
# This function extracts the module name and ensures PYTHONPATH includes the parent directory
154+
_path_to_module() {
155+
local source_path="$1"
156+
local repo_path="${2:-${REPO_PATH}}"
157+
158+
# Remove trailing slash
159+
source_path="${source_path%/}"
160+
161+
# If it's an absolute path, make it relative to repo
162+
if [[ "$source_path" == /* ]]; then
163+
source_path="${source_path#${repo_path}/}"
164+
fi
165+
166+
# Handle common patterns: lib/pkg, src/pkg, backend/app, pkg
167+
# Extract the module name (last component that's a valid Python package)
168+
local module_name=""
169+
local parent_dir=""
170+
171+
if [[ "$source_path" == *"/"* ]]; then
172+
# Path has directory structure (e.g., lib/sqlalchemy, src/mypackage)
173+
parent_dir="${source_path%/*}" # Everything before last /
174+
module_name="${source_path##*/}" # Last component
175+
176+
# Check if the module directory has __init__.py (is a package)
177+
local full_module_path="${repo_path}/${source_path}"
178+
if [[ -f "${full_module_path}/__init__.py" ]]; then
179+
# It's a package - return module name and parent dir
180+
echo "${module_name}|${repo_path}/${parent_dir}"
181+
return 0
182+
fi
183+
184+
# Check subdirectories for packages (e.g., lib/sqlalchemy where sqlalchemy is the package)
185+
for subdir in "${full_module_path}"/*; do
186+
if [[ -d "$subdir" ]] && [[ -f "${subdir}/__init__.py" ]]; then
187+
# Found a package subdirectory
188+
echo "$(basename "$subdir")|${full_module_path}"
189+
return 0
190+
fi
191+
done
192+
fi
193+
194+
# No directory structure or no package found - use the path as-is
195+
# This handles cases like "mypackage" where PYTHONPATH is already set correctly
196+
echo "${source_path}|"
197+
}
198+
151199
run_with_timeout() {
152200
local timeout_secs="$1"
153201
shift
@@ -427,36 +475,68 @@ if [[ "${RUN_CROSSHAIR}" == "1" ]] && command -v crosshair >/dev/null 2>&1; then
427475
if [[ -z "${CROSSHAIR_FILTERED_DIRS}" ]]; then
428476
echo "[sidecar] warning: all source directories filtered out (contain tests), skipping source code analysis"
429477
else
430-
if [[ "${FRAMEWORK_TYPE}" == "django" ]]; then
431-
# Use Django-aware wrapper for source code analysis
432-
CROSSHAIR_WRAPPER="${SIDECAR_DIR}/../frameworks/django/crosshair_django_wrapper.py"
433-
if [[ -f "${CROSSHAIR_WRAPPER}" ]]; then
434-
echo "[sidecar] using Django-aware CrossHair wrapper for source analysis"
435-
# Export environment variables for Django initialization
436-
CROSSHAIR_ENV=""
437-
if [[ -n "${DJANGO_SETTINGS_MODULE:-}" ]]; then
438-
CROSSHAIR_ENV="DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
478+
# Convert source paths to module names for CrossHair
479+
# CrossHair expects module names (e.g., "sqlalchemy") not paths (e.g., "lib/sqlalchemy")
480+
CROSSHAIR_MODULES=""
481+
CROSSHAIR_EXTRA_PYTHONPATH=""
482+
for src_dir in ${CROSSHAIR_FILTERED_DIRS}; do
483+
MODULE_INFO=$(_path_to_module "$src_dir" "${REPO_PATH}")
484+
MODULE_NAME="${MODULE_INFO%%|*}"
485+
MODULE_PARENT="${MODULE_INFO##*|}"
486+
487+
if [[ -n "$MODULE_NAME" ]]; then
488+
CROSSHAIR_MODULES="${CROSSHAIR_MODULES} ${MODULE_NAME}"
489+
if [[ -n "$MODULE_PARENT" ]] && [[ ":${CROSSHAIR_EXTRA_PYTHONPATH}:" != *":${MODULE_PARENT}:"* ]]; then
490+
CROSSHAIR_EXTRA_PYTHONPATH="${CROSSHAIR_EXTRA_PYTHONPATH}:${MODULE_PARENT}"
491+
fi
439492
fi
440-
if [[ -n "${REPO_PATH:-}" ]]; then
441-
CROSSHAIR_ENV="${CROSSHAIR_ENV}REPO_PATH=${REPO_PATH} "
493+
done
494+
CROSSHAIR_MODULES="${CROSSHAIR_MODULES# }" # Trim leading space
495+
CROSSHAIR_EXTRA_PYTHONPATH="${CROSSHAIR_EXTRA_PYTHONPATH#:}" # Trim leading colon
496+
497+
if [[ -z "${CROSSHAIR_MODULES}" ]]; then
498+
echo "[sidecar] warning: could not convert source directories to modules, skipping source code analysis"
499+
else
500+
echo "[sidecar] analyzing modules: ${CROSSHAIR_MODULES}"
501+
if [[ -n "${CROSSHAIR_EXTRA_PYTHONPATH}" ]]; then
502+
echo "[sidecar] extra PYTHONPATH: ${CROSSHAIR_EXTRA_PYTHONPATH}"
442503
fi
443-
if [[ -n "${PYTHONPATH:-}" ]]; then
444-
CROSSHAIR_ENV="${CROSSHAIR_ENV}PYTHONPATH=${PYTHONPATH} "
504+
505+
# Build PYTHONPATH for CrossHair (include extra paths for module resolution)
506+
CROSSHAIR_PYTHONPATH="${PYTHONPATH:-}"
507+
if [[ -n "${CROSSHAIR_EXTRA_PYTHONPATH}" ]]; then
508+
CROSSHAIR_PYTHONPATH="${CROSSHAIR_EXTRA_PYTHONPATH}:${CROSSHAIR_PYTHONPATH}"
509+
fi
510+
511+
if [[ "${FRAMEWORK_TYPE}" == "django" ]]; then
512+
# Use Django-aware wrapper for source code analysis
513+
CROSSHAIR_WRAPPER="${SIDECAR_DIR}/../frameworks/django/crosshair_django_wrapper.py"
514+
if [[ -f "${CROSSHAIR_WRAPPER}" ]]; then
515+
echo "[sidecar] using Django-aware CrossHair wrapper for source analysis"
516+
# Export environment variables for Django initialization
517+
CROSSHAIR_ENV=""
518+
if [[ -n "${DJANGO_SETTINGS_MODULE:-}" ]]; then
519+
CROSSHAIR_ENV="DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
520+
fi
521+
if [[ -n "${REPO_PATH:-}" ]]; then
522+
CROSSHAIR_ENV="${CROSSHAIR_ENV}REPO_PATH=${REPO_PATH} "
523+
fi
524+
CROSSHAIR_ENV="${CROSSHAIR_ENV}PYTHONPATH=${CROSSHAIR_PYTHONPATH} "
525+
run_and_log "${TIMEOUT_CROSSHAIR}" \
526+
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
527+
env ${CROSSHAIR_ENV}"${PYTHON_CMD}" "${CROSSHAIR_WRAPPER}" check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_MODULES}
528+
else
529+
echo "[sidecar] warning: Django wrapper not found, using standard CrossHair (may fail)"
530+
run_and_log "${TIMEOUT_CROSSHAIR}" \
531+
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
532+
env PYTHONPATH="${CROSSHAIR_PYTHONPATH}" "${PYTHON_CMD}" -m crosshair check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_MODULES}
533+
fi
534+
else
535+
# Standard CrossHair for non-Django projects
536+
run_and_log "${TIMEOUT_CROSSHAIR}" \
537+
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
538+
env PYTHONPATH="${CROSSHAIR_PYTHONPATH}" "${PYTHON_CMD}" -m crosshair check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_MODULES}
445539
fi
446-
run_and_log "${TIMEOUT_CROSSHAIR}" \
447-
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
448-
env ${CROSSHAIR_ENV}"${PYTHON_CMD}" "${CROSSHAIR_WRAPPER}" check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_FILTERED_DIRS}
449-
else
450-
echo "[sidecar] warning: Django wrapper not found, using standard CrossHair (may fail)"
451-
run_and_log "${TIMEOUT_CROSSHAIR}" \
452-
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
453-
"${PYTHON_CMD}" -m crosshair check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_FILTERED_DIRS}
454-
fi
455-
else
456-
# Standard CrossHair for non-Django projects
457-
run_and_log "${TIMEOUT_CROSSHAIR}" \
458-
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-source.log" \
459-
"${PYTHON_CMD}" -m crosshair check "${CROSSHAIR_ARGS[@]}" ${CROSSHAIR_FILTERED_DIRS}
460540
fi
461541
fi
462542
fi
@@ -466,21 +546,30 @@ if [[ "${RUN_CROSSHAIR}" == "1" ]] && command -v crosshair >/dev/null 2>&1; then
466546
# This is the primary analysis method for frameworks without decorators (Django, etc.)
467547
if [[ -f "${HARNESS_PATH}" ]]; then
468548
echo "[sidecar] crosshair (harness - external contracts)..."
549+
550+
# Build PYTHONPATH for harness analysis:
551+
# 1. Sidecar directory (for harness imports like 'common.adapters')
552+
# 2. Original PYTHONPATH (for repo modules)
553+
HARNESS_DIR="$(cd "$(dirname "${HARNESS_PATH}")" && pwd)"
554+
HARNESS_FILE="$(basename "${HARNESS_PATH}")"
555+
HARNESS_MODULE="${HARNESS_FILE%.py}" # Remove .py extension
556+
557+
# Build PYTHONPATH: sidecar dir + original PYTHONPATH
558+
HARNESS_PYTHONPATH="${HARNESS_DIR}"
559+
if [[ -n "${PYTHONPATH:-}" ]]; then
560+
HARNESS_PYTHONPATH="${HARNESS_PYTHONPATH}:${PYTHONPATH}"
561+
fi
562+
469563
# Export environment variables for CrossHair subprocess
470-
CROSSHAIR_ENV=""
564+
CROSSHAIR_ENV="PYTHONPATH=${HARNESS_PYTHONPATH} "
471565
if [[ -n "${DJANGO_SETTINGS_MODULE:-}" ]]; then
472-
CROSSHAIR_ENV="DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
566+
CROSSHAIR_ENV="${CROSSHAIR_ENV}DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
473567
fi
474568
if [[ -n "${REPO_PATH:-}" ]]; then
475569
CROSSHAIR_ENV="${CROSSHAIR_ENV}REPO_PATH=${REPO_PATH} "
476570
fi
477-
if [[ -n "${PYTHONPATH:-}" ]]; then
478-
CROSSHAIR_ENV="${CROSSHAIR_ENV}PYTHONPATH=${PYTHONPATH} "
479-
fi
571+
480572
# Change to harness directory to ensure valid module name (avoids hyphenated directory names in module path)
481-
HARNESS_DIR="$(dirname "${HARNESS_PATH}")"
482-
HARNESS_FILE="$(basename "${HARNESS_PATH}")"
483-
HARNESS_MODULE="${HARNESS_FILE%.py}" # Remove .py extension
484573
run_and_log "${TIMEOUT_CROSSHAIR}" \
485574
"${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-harness.log" \
486575
bash -c "cd '${HARNESS_DIR}' && env ${CROSSHAIR_ENV}${PYTHON_CMD} -m crosshair check ${CROSSHAIR_ARGS[*]} ${HARNESS_MODULE}"

0 commit comments

Comments
 (0)