$sanitized_line) { $line_number = $line_num + 1; // Skip if the line is empty in sanitized version if (trim($sanitized_line) === '') { continue; } $original_line = $original_lines[$line_num] ?? $sanitized_line; // Pattern to match variable assignments // Captures: 1=var declaration, 2=variable name, 3=right side expression $pattern = '/(?:^|\s)((?:let\s+|const\s+|var\s+)?)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+?)(?:;|$)/'; if (preg_match($pattern, $sanitized_line, $matches)) { $var_decl = $matches[1]; $var_name = $matches[2]; $right_side = trim($matches[3]); $has_dollar = str_starts_with($var_name, '$'); // Analyze the right side to determine if it returns jQuery object or scalar $expected_type = $this->analyze_expression($right_side); if ($expected_type === 'jquery') { // Should have $ prefix if (!$has_dollar) { $this->add_violation( $file_path, $line_number, "jQuery object must be stored in variable starting with $.", trim($original_line), "Rename variable '{$var_name}' to '\${$var_name}'. " . "The expression returns a jQuery object and must be stored in a variable with $ prefix. " . "In RSpade, $ prefix indicates jQuery objects only.", 'medium' ); } } elseif ($expected_type === 'scalar') { // Should NOT have $ prefix if ($has_dollar) { $this->add_violation( $file_path, $line_number, "Scalar values should not use $ prefix.", trim($original_line), "Remove $ prefix from variable '{$var_name}'. Rename to '" . substr($var_name, 1) . "'. " . "The expression returns a scalar value (string, number, boolean, or DOM element), not a jQuery object. " . "In RSpade, $ prefix is reserved for jQuery objects only.", 'medium' ); } } // If expected_type is 'unknown', we don't enforce either way } } } /** * Analyze an expression to determine if it returns jQuery object or scalar * @return string 'jquery', 'scalar', or 'unknown' */ private function analyze_expression(string $expr): string { $expr = trim($expr); // Direct jQuery selector: $(...) if (preg_match('/^\$\s*\(/', $expr)) { // Check if followed by method chain if (preg_match('/^\$\s*\([^)]*\)(.*)/', $expr, $matches)) { $chain = trim($matches[1]); if ($chain === '') { return 'jquery'; // Just $(...) with no methods } return $this->analyze_method_chain($chain); } return 'jquery'; } // Variable starting with $ (assumed to be jQuery) if (preg_match('/^\$[a-zA-Z_][a-zA-Z0-9_]*(.*)/', $expr, $matches)) { $chain = trim($matches[1]); if ($chain === '') { return 'jquery'; // Just $variable with no methods } if (str_starts_with($chain, '[')) { // Array access like $element[0] return 'scalar'; } return $this->analyze_method_chain($chain); } // Everything else is unknown or definitely not jQuery return 'unknown'; } /** * Analyze a method chain to determine final return type * @param string $chain The method chain starting with . or [ * @return string 'jquery', 'scalar', or 'unknown' */ private function analyze_method_chain(string $chain): string { if (empty($chain)) { return 'jquery'; // No methods means original jQuery object } // Array access [0] or [index] returns DOM element (scalar) if (preg_match('/^\[[\d]+\]/', $chain)) { return 'scalar'; } // Find the last method call in the chain // Match patterns like .method() or .method(args) $methods = []; preg_match_all('/\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)/', $chain, $methods); if (empty($methods[1])) { // No method calls found return 'unknown'; } // Check the last method to determine return type $last_method = end($methods[1]); if (in_array($last_method, self::JQUERY_OBJECT_METHODS, true)) { return 'jquery'; } if (in_array($last_method, self::SCALAR_METHODS, true)) { return 'scalar'; } // Unknown method - could be custom plugin return 'unknown'; } }