codeant-ai-for-open-source[bot] commented on code in PR #37499:
URL: https://github.com/apache/superset/pull/37499#discussion_r2736418026


##########
superset-frontend/src/SqlLab/components/SqlEditor/index.tsx:
##########
@@ -429,73 +429,132 @@ const SqlEditor: FC<Props> = ({
   }, [dispatch, queryEditor.sql, startQuery, stopQuery, formatCurrentQuery]);
 
   const hotkeys = useMemo(() => {
-    // Get all hotkeys including ace editor hotkeys
+    // Get all hotkeys including editor hotkeys
     // Get the user's OS
     const userOS = detectOS();
+
+    type EditorHandle = editors.EditorHandle;
+
+    /**
+     * Find the position of a semicolon in the given direction from a starting 
position.
+     * Returns the position after the semicolon (for backwards) or at the 
semicolon (for forwards).
+     */
+    const findSemicolon = (
+      lines: string[],
+      fromLine: number,
+      fromColumn: number,
+      backwards: boolean,
+    ): { line: number; column: number } | null => {
+      if (backwards) {
+        // Search backwards: start from current position going up
+        for (let line = fromLine; line >= 0; line -= 1) {
+          const lineText = lines[line];
+          const searchEnd = line === fromLine ? fromColumn : lineText.length;
+          // Search from right to left within the line
+          const idx = lineText.lastIndexOf(';', searchEnd - 1);
+          if (idx !== -1) {
+            // Return position after the semicolon
+            return { line, column: idx + 1 };
+          }
+        }
+      } else {
+        // Search forwards: start from current position going down
+        for (let line = fromLine; line < lines.length; line += 1) {
+          const lineText = lines[line];
+          const searchStart = line === fromLine ? fromColumn + 1 : 0;
+          const idx = lineText.indexOf(';', searchStart);
+          if (idx !== -1) {
+            // Return position at the semicolon (end of statement)
+            return { line, column: idx + 1 };
+          }
+        }
+      }
+      return null;
+    };
+
     const base = [
       ...getHotkeyConfig(),
       {
         name: 'runQuery3',
         key: KeyboardShortcut.CtrlShiftEnter,
         descr: KEY_MAP[KeyboardShortcut.CtrlShiftEnter],
-        func: (editor: AceEditor['editor']) => {
-          if (!editor.getValue().trim()) {
+        func: (editor: EditorHandle) => {
+          const value = editor.getValue();
+          if (!value.trim()) {
             return;
           }
-          const session = editor.getSession();
+
+          const lines = value.split('\n');
           const cursorPosition = editor.getCursorPosition();
-          const totalLine = session.getLength();
-          const currentRow = editor.getFirstVisibleRow();
-          const semicolonEnd = editor.find(';', {
-            backwards: false,
-            skipCurrent: true,
-          });
-          let end;
-          if (semicolonEnd) {
-            ({ end } = semicolonEnd);
-          }
-          if (!end || end.row < cursorPosition.row) {
-            end = {
-              row: totalLine + 1,
-              column: 0,
-            };
+          const totalLines = lines.length;
+
+          // Find the end of the statement (next semicolon or end of file)
+          const semicolonEnd = findSemicolon(
+            lines,
+            cursorPosition.line,
+            cursorPosition.column,
+            false,
+          );
+          let end: { line: number; column: number };
+          if (semicolonEnd && semicolonEnd.line >= cursorPosition.line) {

Review Comment:
   **Suggestion:** Off-by-one / out-of-bounds selection: when no semicolon is 
found the code sets the end position to `{ line: totalLines, column: 0 }`. 
`totalLines` is `lines.length` so the highest valid line index is `totalLines - 
1`. Using `line: totalLines` produces an invalid position and can cause the 
editor selection APIs to throw or behave unpredictably. Use the last line index 
and its length as the fallback end position (handle empty content safely). 
[off-by-one]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Ctrl+Shift+Enter runs current-statement selection fails.
   - ⚠️ SqlLab editor hotkey may throw runtime selection errors.
   - ⚠️ Affects all editor providers via EditorWrapper selection API.
   ```
   </details>
   
   ```suggestion
                 // No semicolon found forward, use end of file (last line 
index)
                 if (totalLines === 0) {
                   end = { line: 0, column: 0 };
                 } else {
                   const lastLineIndex = totalLines - 1;
                   end = {
                     line: lastLineIndex,
                     column: lines[lastLineIndex]?.length ?? 0,
                   };
                 }
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open SqlLab and focus an editor tab that uses SqlEditor component:
   
      superset-frontend/src/SqlLab/components/SqlEditor/index.tsx lines 
~429-559 (runQuery3
      hotkey logic).
   
   2. Place the cursor in a SQL buffer that contains no semicolon after the 
cursor
   
      (typical for single-statement queries without trailing semicolons). This 
triggers
   
      the run-statement hotkey path when pressing Ctrl+Shift+Enter.
   
   3. The hotkey handler (runQuery3) at
   
      superset-frontend/src/SqlLab/components/SqlEditor/index.tsx:496-500 
executes the
      fallback
   
      branch and sets end = { line: totalLines, column: 0 } where totalLines ===
      lines.length.
   
   4. Because valid line indices are 0..lines.length-1, setting line: 
totalLines produces an
   
      out-of-range cursor/selection position when EditorHandle.setSelection() 
is later
      invoked
   
      (editor.setSelection(...) after line 540 in the same file). This can 
cause the editor
   
      provider's selection API to throw or behave unpredictably.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
   **Line:** 499:499
   **Comment:**
        *Off By One: Off-by-one / out-of-bounds selection: when no semicolon is 
found the code sets the end position to `{ line: totalLines, column: 0 }`. 
`totalLines` is `lines.length` so the highest valid line index is `totalLines - 
1`. Using `line: totalLines` produces an invalid position and can cause the 
editor selection APIs to throw or behave unpredictably. Use the last line index 
and its length as the fallback end position (handle empty content safely).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx:
##########
@@ -0,0 +1,365 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+import { usePrevious } from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { Global } from '@emotion/react';
+import type { editors } from '@apache-superset/core';
+
+import { SQL_EDITOR_LEFTBAR_WIDTH } from 'src/SqlLab/constants';
+import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab';
+import type { KeyboardShortcut } from 
'src/SqlLab/components/KeyboardShortcutButton';
+import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
+import { SqlLabRootState, type CursorPosition } from 'src/SqlLab/types';
+import { EditorHost } from 'src/core/editors';
+import { useAnnotations } from './useAnnotations';
+import { useKeywords } from './useKeywords';
+
+type EditorHandle = editors.EditorHandle;
+type EditorHotkey = editors.EditorHotkey;
+type EditorAnnotation = editors.EditorAnnotation;
+
+type HotKey = {
+  key: KeyboardShortcut;
+  descr?: string;
+  name: string;
+  func: (editor: EditorHandle) => void;
+};
+
+type EditorWrapperProps = {
+  autocomplete: boolean;
+  onBlur: (sql: string) => void;
+  onChange: (sql: string) => void;
+  queryEditorId: string;
+  onCursorPositionChange: (position: CursorPosition) => void;
+  height: string;
+  hotkeys: HotKey[];
+};
+
+/**
+ * Convert legacy HotKey format to EditorHotkey format.
+ */
+const convertHotkeys = (
+  hotkeys: HotKey[],
+  onRunQuery: () => void,
+): EditorHotkey[] => {
+  const result: EditorHotkey[] = [
+    // Add the built-in run query hotkey
+    {
+      name: 'runQuery',
+      key: 'Alt-enter',
+      description: 'Run query',
+      exec: () => onRunQuery(),
+    },
+  ];
+
+  hotkeys.forEach(keyConfig => {
+    result.push({
+      name: keyConfig.name,
+      key: keyConfig.key,
+      description: keyConfig.descr,
+      exec: keyConfig.func,
+    });
+  });
+
+  return result;
+};
+
+/**
+ * Ace annotation format returned from useAnnotations when data is available.
+ */
+type AceAnnotation = {
+  row: number;
+  column: number;
+  text: string | null;
+  type: string;
+};
+
+/**
+ * Type guard to check if an annotation is in Ace format.
+ */
+const isAceAnnotation = (ann: unknown): ann is AceAnnotation =>
+  typeof ann === 'object' &&
+  ann !== null &&
+  'row' in ann &&
+  'column' in ann &&
+  'text' in ann &&
+  'type' in ann;
+
+/**
+ * Convert annotation array to EditorAnnotation format.
+ * Handles the union type returned from useAnnotations.
+ */
+const convertAnnotations = (
+  annotations?: unknown[],
+): EditorAnnotation[] | undefined => {
+  if (!annotations || annotations.length === 0) return undefined;
+  // Check if first item is in Ace format (has row, column, text, type)
+  if (!isAceAnnotation(annotations[0])) return undefined;
+  return (annotations as AceAnnotation[]).map(ann => ({
+    line: ann.row,
+    column: ann.column,
+    message: ann.text ?? '',
+    severity: ann.type as EditorAnnotation['severity'],
+  }));
+};
+
+/**
+ * EditorWrapper component that renders the SQL editor using EditorHost.
+ * Uses the default Ace editor or an extension-provided editor based on
+ * what's registered with the editors API.
+ */
+const EditorWrapper = ({
+  autocomplete,
+  onBlur = () => {},
+  onChange = () => {},
+  queryEditorId,
+  onCursorPositionChange,
+  height,
+  hotkeys,
+}: EditorWrapperProps) => {
+  const dispatch = useDispatch();
+  const queryEditor = useQueryEditor(queryEditorId, [
+    'id',
+    'dbId',
+    'sql',
+    'catalog',
+    'schema',
+    'templateParams',
+    'tabViewId',
+  ]);
+  // Prevent a maximum update depth exceeded error
+  // by skipping access the unsaved query editor state
+  const cursorPosition = useSelector<SqlLabRootState, CursorPosition>(
+    ({ sqlLab: { queryEditors } }) => {
+      const { cursorPosition: pos } = {
+        ...queryEditors.find(({ id }) => id === queryEditorId),
+      };

Review Comment:
   **Suggestion:** Spreading the result of `queryEditors.find(...)` can throw a 
TypeError if no editor is found (the find returns undefined and spreading 
undefined raises). Safely handle a missing find result before 
spreading/destructuring to avoid runtime crashes. [type error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ SQL Lab query editor crashes on mount.
   - ⚠️ Any EditorHost consumer may unmount unexpectedly.
   - ⚠️ Redux selector errors surface in UI render.
   ```
   </details>
   
   ```suggestion
         const found = queryEditors.find(({ id }) => id === queryEditorId) ?? 
{};
         const { cursorPosition: pos } = found as Partial<{ cursorPosition?: 
CursorPosition }>;
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Mount the EditorWrapper component in the running app (file:
   superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx) so the 
component's render
   executes the selector at lines 150-158.
   
   2. Ensure the Redux slice sqlLab.queryEditors does NOT contain an editor 
with the prop
   queryEditorId passed to EditorWrapper. The selector's callback at lines 
151-156 calls
   queryEditors.find(({ id }) => id === queryEditorId) (line 153).
   
   3. Because Array.prototype.find returns undefined when no match exists, the 
code spreads
   that undefined into an object literal at line 152-154 ("const { 
cursorPosition: pos } = {
   ...queryEditors.find(...) }"), which throws a TypeError during the 
useSelector callback.
   
   4. Observe the component throwing at runtime during render (stack trace will 
point to
   superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx:152-154). 
This prevents
   the EditorWrapper from mounting and breaks the SQL Lab editor UI.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx
   **Line:** 152:154
   **Comment:**
        *Type Error: Spreading the result of `queryEditors.find(...)` can throw 
a TypeError if no editor is found (the find returns undefined and spreading 
undefined raises). Safely handle a missing find result before 
spreading/destructuring to avoid runtime crashes.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to