Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/rss-reader/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ function decodeEntities(value: string): string {

return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity: string) => {
if (entity.startsWith('#x')) {
const codePoint = Number.parseInt(entity.slice(2), 16);
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
}

if (entity.startsWith('#')) {
const codePoint = Number.parseInt(entity.slice(1), 10);
const codePoint = Number.parseInt(entity.slice(1, 10), 10);
Comment on lines +30 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)index\.tsx$|package.json|tsconfig' | head -200

echo
echo "Target file excerpt:"
if [ -f examples/rss-reader/src/index.tsx ]; then
  nl -ba examples/rss-reader/src/index.tsx | sed -n '1,80p'
fi

echo
echo "Search numeric entity decode logic:"
rg -n "fromCodePoint|codePoint|parseInt|slashEncode|replace.*entities|entities" examples/rss-reader/src examples 2>/dev/null | head -200

echo
echo "Behavioral probe for current slicing and fromCodePoint behavior:"
node - <<'JS'
const candidates = [
  '22fffff',
  '999999999',
  '10ffff',
  '110000',
  '0xfffffffffffffffffffffffffff',
  'abc',
];
for (const text of candidates) {
  for (const [radix, sliceStart, sliceEnd] of [
    ['16', 2, 10],
    ['10', 1, 10]
  ]) {
    const entity = '#' + text;
    const s = entity.slice(sliceStart, sliceEnd);
    let codePoint;
    let thrown;
    try {
      codePoint = Number.parseInt(s, radix);
      const ok = Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
      if (ok !== null) console.log(`${text} radix${radix} first${sliceEnd-1} digits as utf16 ok`);
      else console.log(`${text} radix${radix} first${sliceEnd-1} digits NaN`);
    } catch (err) {
      thrown = err && err.toString ? err.toString().split('\n')[0] : String(err);
      console.log(`${text} radix${radix} first${sliceEnd-1} digits parsed ${codePoint} throws ${thrown}`);
    }
  }
  let codePoint = Number.parseInt(text, 16);
  try { String.fromCodePoint(codePoint); console.log(`${text} parsed ${codePoint} fromCodePoint ok`); }
  catch (err) { console.log(`${text} parsed ${codePoint} fromCodePoint throws ${err.toString().split('\n')[0]}`); }
}
JS

Repository: Karanjot786/TermUI

Length of output: 5143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file excerpt:"
awk '{printf("%6d: %s\n", NR, $0)}' examples/rss-reader/src/index.tsx | sed -n '1,90p'

echo
echo "Search numeric entity decode logic:"
rg -n "fromCodePoint|codePoint|parseInt|entities|slice\\(.*10|replace.*&#|`#x`?\\[" examples/rss-reader/src examples 2>/dev/null | head -200

echo
echo "Behavioral probe for current slicing and fromCodePoint behavior:"
node - <<'JS'
const regex = /&(`#x`?[0-9a-fA-F]+|[a-zA-Z]+);/g;
const sliceFn = (entity) => {
  if (entity.startsWith('`#x`')) {
    const codePoint = Number.parseInt(entity.slice(2, 10), 16);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  if (entity.startsWith('#')) {
    const codePoint = Number.parseInt(entity.slice(1, 10), 10);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  return null;
};
const candidates = [
  '&`#22fffff`;',
  '&`#999999999`;',
  '&`#10ffff`;',
  '&`#110000`;',
  '&`#x0xfffffffffffffffffffffffffff`;',
  '&`#xabcdefg`;',
];
for (const text of candidates) {
  const entity = text.replace(/^&#|;$/, '');
  console.log(text + ' entity=' + entity);
  console.log('    regexMatches=', !!entity.match(/^`#x`?[0-9a-fA-F]+$/));
  let thrown;
  try {
    console.log('    decoded=' + sliceFn(entity));
  } catch (err) {
    thrown = err && err.toString ? err.toString().split('\n')[0] : String(err);
    console.log('    throws=' + thrown);
  }
}
JS

Repository: Karanjot786/TermUI

Length of output: 5595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Behavioral probe with correct entity suffix slicing:"
node - <<'JS'
function currentDecode(entity) {
  if (entity.startsWith('`#x`')) {
    const codePoint = Number.parseInt(entity.slice(2, 10), 16);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  if (entity.startsWith('#')) {
    const codePoint = Number.parseInt(entity.slice(1, 10), 10);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  return null;
}
function wholeEntityDecode(entity) {
  if (entity.startsWith('`#x`')) {
    const codePoint = Number.parseInt(entity.slice(2), 16);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  if (entity.startsWith('#')) {
    const codePoint = Number.parseInt(entity.slice(1), 10);
    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : null;
  }
  return null;
}
for (const entity of ['22fffff', '999999999', '10ffff', '110000', '0xfffffffffffffffffffffffffff']) {
  console.log(`##${entity}: regexMatch=${/^`#x`?[0-9a-fA-F]+$/.test(entity)} code=${currentDecode(entity)} whole=${wholeEntityDecode(entity)}`);
}
for (const entity of ['22fffff', '999999999', '10ffff', '110000', '0xfffffffffffffffffffffffffff']) {
  console.log(`x0#${entity}: hexRegexMatch=${/^[0-9a-fA-F]+$/.test(entity)} code=${currentDecode(entity)} whole=${wholeEntityDecode(entity)}`);
}
JS

Repository: Karanjot786/TermUI

Length of output: 759


Reject overlong and out-of-range numeric entities before decoding.

slice(..., 10) takes nine characters after the #; decimal entities like &#999999999; can decode from a prefix. Hex decoding slices after #x, so malformed hex-only input or excessively long hex values can also pass the predicate. Limit the regex to at most eight digits, parse the complete entity suffix, and reject values above 0x10FFFF before calling String.fromCodePoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/rss-reader/src/index.tsx` around lines 30 - 35, Update the
numeric-entity handling in the entity-decoding function to match at most eight
digits, parse the complete suffix rather than a truncated slice, and reject
values outside the valid Unicode range, including values above 0x10FFFF, before
calling String.fromCodePoint. Apply the same validation to both decimal entities
and hexadecimal entities while preserving the existing fallback to match for
invalid input.

return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
}

Expand Down Expand Up @@ -188,7 +188,7 @@ function ErrorScreen({ message }: { message: string }) {

function FeedListPane({ items, state }: { items: FeedEntry[]; state: ReturnType<typeof useListState> }) {
const listRef = useRef<List | null>(null);
const mappedItems: ListItem[] = items.map((entry) => ({ label: entry.title, value: entry.link }));
const mappedItems: ListItem[] = (items ?? []).map((entry) => ({ label: entry.title, value: entry.link }));

const list = listRef.current ??= new List(
{ items: mappedItems, state },
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class ShowcaseApp extends Widget {
if (event.key === 'q' || (event.ctrl && event.key === 'c')) return false;

// Tab switching: 1-5
const num = parseInt(event.key);
const num = parseInt(event.key, 10);
if (num >= 1 && num <= 5) {
this.switchTab(num - 1);
return true;
Expand Down
2 changes: 1 addition & 1 deletion examples/widget-gallery/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class WidgetGalleryApp extends Widget {
}

// Tab switching: 1-6
const num = parseInt(event.key);
const num = parseInt(event.key, 10);
if (num >= 1 && num <= 6) {
this._switchTab(num - 1);
return true;
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/TreeSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ function _pathsEqual(a: number[], b: number[]): boolean {

function _valuesEqual(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const sortedA = [...a].sort();
const sortedB = [...b].sort();
const sortedA = [...a].sort((a, b) => a - b);
const sortedB = [...b].sort((a, b) => a - b);
Comment on lines +185 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(TreeSelect|build-registry)\.(ts|tsx)$|packages/tsconfig|scripts/.*ts$' || true

echo "== TreeSelect relevant sections =="
if [ -f packages/ui/src/TreeSelect.ts ]; then
  wc -l packages/ui/src/TreeSelect.ts
  sed -n '150,210p' packages/ui/src/TreeSelect.ts
fi

echo "== build-registry relevant sections =="
if [ -f scripts/build-registry.ts ]; then
  wc -l scripts/build-registry.ts
  sed -n '30,65p' scripts/build-registry.ts
fi

echo "== TypeScript strict config snippets =="
for f in packages/ui/tsconfig.json tsconfig.json packages/tsconfig.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat "$f"
  fi
done

echo "== package scripts / dependency type context =="
if [ -f scripts/build-registry.ts ]; then
  rg -n "deps|dependenc|sorted|sort\\(" scripts/build-registry.ts packages --glob '*.ts' --glob '*.tsx' -C 2 || true
fi

echo "== standalone JS subtraction comparator behavior =="
node - <<'JS'
const values = ["apples", "bananas", "Oranges"];
const sortedSub = [...values].sort((a, b) => a - b);
const sortedLex = [...values].sort((a, b) => a.localeCompare(b));
console.log(JSON.stringify({ sortedSub, sortedLex }));
for (const pair of [["a", "b"], ["1", "2"], ["Oranges", "apples"]]) {
  const [a, b] = pair;
  console.log(`${a} - ${b} = ${a - b}`);
}
JS

Repository: Karanjot786/TermUI

Length of output: 50374


Use a string comparator for the sorted selection comparisons.

_valuesEqual compares string arrays, but a - b converts the elements to numbers and returns NaN when either value is non-numeric. Replace both sort comparators with a.localeCompare(b).

📍 Affects 2 files
  • packages/ui/src/TreeSelect.ts#L185-L186 (this comment)
  • scripts/build-registry.ts#L47-L47
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/TreeSelect.ts` around lines 185 - 186, Update both sorted
selection comparisons in packages/ui/src/TreeSelect.ts (lines 185-186) and
scripts/build-registry.ts (line 47) to use string comparators via localeCompare
instead of numeric subtraction. Preserve the existing _valuesEqual comparison
behavior while correctly sorting non-numeric string values.

Source: Coding guidelines

for (let i = 0; i < sortedA.length; i++) {
if (sortedA[i] !== sortedB[i]) return false;
}
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function collectDeps(content: string): string[] {
const deps = new Set<string>();
let m: RegExpExecArray | null;
while ((m = re.exec(content)) !== null) deps.add(m[1]!);
return [...deps].sort();
return [...deps].sort((a, b) => a - b);
}

export function toSlug(name: string): string {
Expand Down Expand Up @@ -289,7 +289,7 @@ function parseOptionsInterface(content: string, optionsTypeName: string): ApiPro
}
const local = parseFields(m[2]!);
// Prepend inherited fields; a locally-redeclared field overrides the parent.
const localNames = new Set(local.map(p => p.name));
const localNames = new Set((local ?? []).map(p => p.name));
return [...inherited.filter(p => !localNames.has(p.name)), ...local];
}

Expand Down
Loading