All files / src string-classifier.ts

97.72% Statements 43/44
94.54% Branches 52/55
100% Functions 3/3
97.67% Lines 42/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302                          7x   7x   7x   7x           7x                                                                                                                                                                                         7x                                                                                                           7x                                           7x                   46x 2x       44x 384x 11x         33x 33x 1x         32x   2x 2x         30x         30x         4x       26x       2x       24x     24x 15x 15x 10x         14x 4x       10x 11x 4x         6x 6x       6x       6x 1x         5x         4x     1x    
/**
 * String Literal Classifier for AI Signal Clarity.
 *
 * Classifies string literals into categories to distinguish between:
 * - Meaningful magic literals (should be extracted into named constants)
 * - UI/Display strings (OK to keep inline)
 *
 * @lastUpdated 2026-03-20
 */
 
/**
 * Categories for string literal classification
 */
export enum StringCategory {
  /** Configuration values, API endpoints, business logic constants - should extract */
  Meaningful = 'meaningful',
  /** UI strings like button labels, tooltips, headers - OK to keep inline */
  UiDisplay = 'ui-display',
  /** Strings that should be ignored entirely */
  Ignore = 'ignore',
}
 
/**
 * Common UI action words that indicate a string is likely a UI label/action
 */
const UI_ACTION_WORDS = new Set([
  'click',
  'tap',
  'press',
  'select',
  'choose',
  'add',
  'remove',
  'delete',
  'save',
  'cancel',
  'close',
  'submit',
  'ok',
  'yes',
  'no',
  'next',
  'previous',
  'back',
  'continue',
  'finish',
  'done',
  'loading',
  'please',
  'wait',
  'help',
  'about',
  'settings',
  'options',
  'menu',
  'home',
  'search',
  'filter',
  'sort',
  'refresh',
  'reset',
  'clear',
  'undo',
  'redo',
  'copy',
  'paste',
  'cut',
  'edit',
  'view',
  'show',
  'hide',
  'expand',
  'collapse',
  'open',
  'new',
  'create',
  'update',
  'modify',
  'change',
  'apply',
  'confirm',
  'decline',
  'accept',
  'reject',
  'retry',
  'ignore',
  'skip',
  'start',
  'stop',
  'pause',
  'resume',
  'play',
  'record',
  'upload',
  'download',
  'import',
  'export',
  'print',
  'share',
  'send',
  'receive',
  'login',
  'logout',
  'sign',
  'register',
  'subscribe',
  'unsubscribe',
  'enable',
  'disable',
  'activate',
  'deactivate',
  'lock',
  'unlock',
]);
 
/**
 * Common UI nouns that indicate a string is likely a UI label
 */
const UI_NOUN_WORDS = new Set([
  'error',
  'success',
  'warning',
  'info',
  'notice',
  'alert',
  'notification',
  'message',
  'status',
  'result',
  'feedback',
  'comment',
  'note',
  'description',
  'details',
  'summary',
  'overview',
  'name',
  'title',
  'label',
  'placeholder',
  'hint',
  'tip',
  'help',
  'example',
  'sample',
  'preview',
  'demo',
  'test',
  'user',
  'email',
  'password',
  'username',
  'account',
  'profile',
  'settings',
  'preferences',
  'notifications',
  'privacy',
  'security',
  'billing',
  'payment',
  'dashboard',
  'reports',
  'analytics',
  'logs',
  'history',
  'activity',
]);
 
/**
 * Patterns that indicate meaningful (should-extract) strings
 */
const MEANINGFUL_PATTERNS: RegExp[] = [
  // API endpoints and URLs
  /^\/api\//i,
  /^https?:\/\//i,
  /^\/v\d+\//i,
  // Configuration keys
  /^(DATABASE_URL|API_KEY|SECRET|TOKEN|HOST|PORT|TIMEOUT|RETRY|MAX_|MIN_|DEFAULT_)/i,
  // Environment values
  /^(production|development|staging|test|dev|prod|qa)$/i,
  // MIME types
  /^application\//i,
  /^text\//i,
  // HTTP headers (not methods, which are often used as UI labels)
  /^(Authorization|Content-Type|Accept)$/i,
  // Error codes
  /^[A-Z_]+_ERROR$/i,
  /^[A-Z_]+_CODE$/i,
];
 
/**
 * Patterns that indicate UI display strings (should NOT be flagged as magic literals)
 */
const UI_DISPLAY_PATTERNS: RegExp[] = [
  // Strings ending with "..." are typically UI placeholders or expandable actions
  /\.\.\.$/,
];
 
/**
 * Classify a string literal into a category
 */
export function classifyStringLiteral(value: string): StringCategory {
  // Very short or very long strings are typically not UI labels
  if (value.length === 0 || value.length > 100) {
    return StringCategory.Ignore;
  }
 
  // Check if it matches meaningful patterns first
  for (const pattern of MEANINGFUL_PATTERNS) {
    if (pattern.test(value)) {
      return StringCategory.Meaningful;
    }
  }
 
  // Check for UI display patterns first
  for (const pattern of UI_DISPLAY_PATTERNS) {
    if (pattern.test(value)) {
      return StringCategory.UiDisplay;
    }
  }
 
  // Check for URLs, paths, emails (meaningful) - but not if it's clearly a UI string
  if (/[/.@]/.test(value) && !value.includes(' ')) {
    // Don't classify as meaningful if it looks like UI text with dots (e.g., "Filter...")
    Eif (!value.endsWith('...')) {
      return StringCategory.Meaningful;
    }
  }
 
  // Check for numeric strings that look like config values
  Iif (/^\d+(\.\d+)?$/.test(value)) {
    return StringCategory.Meaningful;
  }
 
  // Check for all-caps constants (meaningful) - including short ones like HTTP methods
  if (
    value === value.toUpperCase() &&
    value.length >= 3 &&
    /[A-Z]/.test(value)
  ) {
    return StringCategory.Meaningful;
  }
 
  // Check for camelCase or PascalCase identifiers (meaningful)
  if (
    /^[a-z]+([A-Z][a-z]+)+$/.test(value) ||
    /^([A-Z][a-z]+){2,}$/.test(value)
  ) {
    return StringCategory.Meaningful;
  }
 
  // Split into words for further analysis
  const words = value.toLowerCase().split(/\s+/);
 
  // Check if it's a single word that looks like a UI label
  if (words.length === 1) {
    const word = words[0].replace(/[.]+$/, ''); // Remove trailing dots
    if (UI_ACTION_WORDS.has(word) || UI_NOUN_WORDS.has(word)) {
      return StringCategory.UiDisplay;
    }
  }
 
  // Check if it starts with a UI action word
  if (words.length > 0 && UI_ACTION_WORDS.has(words[0].replace(/[.]+$/, ''))) {
    return StringCategory.UiDisplay;
  }
 
  // Check if it contains UI noun words
  for (const word of words) {
    if (UI_NOUN_WORDS.has(word)) {
      return StringCategory.UiDisplay;
    }
  }
 
  // Short phrases (1-5 words) that are capitalized like titles are likely UI
  Eif (words.length >= 1 && words.length <= 5) {
    const isTitleCase = value
      .split(/\s+/)
      .every(
        (w) =>
          w.length === 0 ||
          /^[A-Z]/.test(w) ||
          /^(a|an|the|and|or|but|in|on|at|to|for|of|with|by)$/i.test(w)
      );
    if (isTitleCase && /[a-zA-Z]/.test(value)) {
      return StringCategory.UiDisplay;
    }
  }
 
  // Default: treat as meaningful if it has reasonable length and looks like it could be a constant
  if (
    value.length >= 3 &&
    value.length <= 50 &&
    /^[a-zA-Z0-9_-]+$/.test(value)
  ) {
    return StringCategory.Meaningful;
  }
 
  return StringCategory.Ignore;
}