baseline-lint
Version:
Check web features for Baseline compatibility
677 lines (620 loc) • 21.5 kB
JavaScript
// src/parsers/js-parser.js
// Parse JavaScript files and check Baseline compatibility
import { parse } from '@babel/parser';
import traverseDefault from '@babel/traverse';
import { checkJavaScriptAPI, generateReport } from '../core/checker.js';
import { ParseError, FileError, handleError, safeAsync } from '../utils/error-handler.js';
import { readFileWithCleanup } from '../utils/file-handler.js';
// @babel/traverse exports a default object, need to get the actual function
const traverse = traverseDefault.default || traverseDefault;
/**
* JavaScript APIs to check (comprehensive modern features)
*/
const JS_APIS = {
// Array methods
'Array.prototype.at': 'at',
'Array.prototype.findLast': 'findLast',
'Array.prototype.findLastIndex': 'findLastIndex',
'Array.prototype.toReversed': 'toReversed',
'Array.prototype.toSorted': 'toSorted',
'Array.prototype.toSpliced': 'toSpliced',
'Array.prototype.with': 'with',
'Array.prototype.flatMap': 'flatMap',
'Array.prototype.flat': 'flat',
'Array.prototype.includes': 'includes',
'Array.prototype.find': 'find',
'Array.prototype.findIndex': 'findIndex',
'Array.prototype.fill': 'fill',
'Array.prototype.copyWithin': 'copyWithin',
'Array.prototype.keys': 'keys',
'Array.prototype.values': 'values',
'Array.prototype.entries': 'entries',
'Array.prototype.from': 'from',
'Array.prototype.of': 'of',
'Array.prototype.isArray': 'isArray',
'Array.prototype.forEach': 'forEach',
'Array.prototype.map': 'map',
'Array.prototype.filter': 'filter',
'Array.prototype.reduce': 'reduce',
'Array.prototype.reduceRight': 'reduceRight',
'Array.prototype.some': 'some',
'Array.prototype.every': 'every',
'Array.prototype.sort': 'sort',
'Array.prototype.reverse': 'reverse',
'Array.prototype.concat': 'concat',
'Array.prototype.join': 'join',
'Array.prototype.slice': 'slice',
'Array.prototype.splice': 'splice',
'Array.prototype.push': 'push',
'Array.prototype.pop': 'pop',
'Array.prototype.shift': 'shift',
'Array.prototype.unshift': 'unshift',
'Array.prototype.indexOf': 'indexOf',
'Array.prototype.lastIndexOf': 'lastIndexOf',
// Promise methods
'Promise.try': 'try',
'Promise.allSettled': 'allSettled',
'Promise.any': 'any',
'Promise.all': 'all',
'Promise.race': 'race',
'Promise.resolve': 'resolve',
'Promise.reject': 'reject',
// String methods
'String.prototype.replaceAll': 'replaceAll',
'String.prototype.at': 'at',
'String.prototype.padStart': 'padStart',
'String.prototype.padEnd': 'padEnd',
'String.prototype.trimStart': 'trimStart',
'String.prototype.trimEnd': 'trimEnd',
'String.prototype.startsWith': 'startsWith',
'String.prototype.endsWith': 'endsWith',
'String.prototype.includes': 'includes',
'String.prototype.repeat': 'repeat',
'String.prototype.codePointAt': 'codePointAt',
'String.prototype.normalize': 'normalize',
'String.prototype.matchAll': 'matchAll',
'String.prototype.replace': 'replace',
'String.prototype.search': 'search',
'String.prototype.split': 'split',
'String.prototype.substring': 'substring',
'String.prototype.substr': 'substr',
'String.prototype.slice': 'slice',
'String.prototype.indexOf': 'indexOf',
'String.prototype.lastIndexOf': 'lastIndexOf',
'String.prototype.charCodeAt': 'charCodeAt',
'String.prototype.fromCharCode': 'fromCharCode',
'String.prototype.fromCodePoint': 'fromCodePoint',
'String.prototype.raw': 'raw',
// Object methods
'Object.hasOwn': 'hasOwn',
'Object.groupBy': 'groupBy',
'Object.fromEntries': 'fromEntries',
'Object.assign': 'assign',
'Object.create': 'create',
'Object.defineProperty': 'defineProperty',
'Object.defineProperties': 'defineProperties',
'Object.freeze': 'freeze',
'Object.seal': 'seal',
'Object.preventExtensions': 'preventExtensions',
'Object.isFrozen': 'isFrozen',
'Object.isSealed': 'isSealed',
'Object.isExtensible': 'isExtensible',
'Object.keys': 'keys',
'Object.values': 'values',
'Object.entries': 'entries',
'Object.getOwnPropertyNames': 'getOwnPropertyNames',
'Object.getOwnPropertySymbols': 'getOwnPropertySymbols',
'Object.getOwnPropertyDescriptor': 'getOwnPropertyDescriptor',
'Object.getOwnPropertyDescriptors': 'getOwnPropertyDescriptors',
'Object.getPrototypeOf': 'getPrototypeOf',
'Object.setPrototypeOf': 'setPrototypeOf',
'Object.is': 'is',
// Number methods
'Number.isFinite': 'isFinite',
'Number.isInteger': 'isInteger',
'Number.isNaN': 'isNaN',
'Number.isSafeInteger': 'isSafeInteger',
'Number.parseFloat': 'parseFloat',
'Number.parseInt': 'parseInt',
'Number.EPSILON': 'EPSILON',
'Number.MAX_SAFE_INTEGER': 'MAX_SAFE_INTEGER',
'Number.MIN_SAFE_INTEGER': 'MIN_SAFE_INTEGER',
'Number.MAX_VALUE': 'MAX_VALUE',
'Number.MIN_VALUE': 'MIN_VALUE',
'Number.POSITIVE_INFINITY': 'POSITIVE_INFINITY',
'Number.NEGATIVE_INFINITY': 'NEGATIVE_INFINITY',
// Math methods
'Math.trunc': 'trunc',
'Math.sign': 'sign',
'Math.cbrt': 'cbrt',
'Math.log2': 'log2',
'Math.log10': 'log10',
'Math.fround': 'fround',
'Math.imul': 'imul',
'Math.clz32': 'clz32',
'Math.hypot': 'hypot',
'Math.acosh': 'acosh',
'Math.asinh': 'asinh',
'Math.atanh': 'atanh',
'Math.sinh': 'sinh',
'Math.cosh': 'cosh',
'Math.tanh': 'tanh',
// RegExp methods
'RegExp.prototype.hasIndices': 'hasIndices',
'RegExp.prototype.dotAll': 'dotAll',
'RegExp.prototype.global': 'global',
'RegExp.prototype.ignoreCase': 'ignoreCase',
'RegExp.prototype.multiline': 'multiline',
'RegExp.prototype.sticky': 'sticky',
'RegExp.prototype.unicode': 'unicode',
'RegExp.prototype.flags': 'flags',
'RegExp.prototype.test': 'test',
'RegExp.prototype.exec': 'exec',
'RegExp.prototype.toString': 'toString',
// Map methods
'Map.prototype.has': 'has',
'Map.prototype.get': 'get',
'Map.prototype.set': 'set',
'Map.prototype.delete': 'delete',
'Map.prototype.clear': 'clear',
'Map.prototype.keys': 'keys',
'Map.prototype.values': 'values',
'Map.prototype.entries': 'entries',
'Map.prototype.forEach': 'forEach',
'Map.prototype.size': 'size',
// Set methods
'Set.prototype.has': 'has',
'Set.prototype.add': 'add',
'Set.prototype.delete': 'delete',
'Set.prototype.clear': 'clear',
'Set.prototype.keys': 'keys',
'Set.prototype.values': 'values',
'Set.prototype.entries': 'entries',
'Set.prototype.forEach': 'forEach',
'Set.prototype.size': 'size',
// WeakMap methods
'WeakMap.prototype.has': 'has',
'WeakMap.prototype.get': 'get',
'WeakMap.prototype.set': 'set',
'WeakMap.prototype.delete': 'delete',
// WeakSet methods
'WeakSet.prototype.has': 'has',
'WeakSet.prototype.add': 'add',
'WeakSet.prototype.delete': 'delete',
// Symbol methods
'Symbol.for': 'for',
'Symbol.keyFor': 'keyFor',
'Symbol.hasInstance': 'hasInstance',
'Symbol.isConcatSpreadable': 'isConcatSpreadable',
'Symbol.iterator': 'iterator',
'Symbol.match': 'match',
'Symbol.replace': 'replace',
'Symbol.search': 'search',
'Symbol.species': 'species',
'Symbol.split': 'split',
'Symbol.toPrimitive': 'toPrimitive',
'Symbol.toStringTag': 'toStringTag',
'Symbol.unscopables': 'unscopables',
// Proxy methods
'Proxy.revocable': 'revocable',
// Reflect methods
'Reflect.apply': 'apply',
'Reflect.construct': 'construct',
'Reflect.defineProperty': 'defineProperty',
'Reflect.deleteProperty': 'deleteProperty',
'Reflect.get': 'get',
'Reflect.getOwnPropertyDescriptor': 'getOwnPropertyDescriptor',
'Reflect.getPrototypeOf': 'getPrototypeOf',
'Reflect.has': 'has',
'Reflect.isExtensible': 'isExtensible',
'Reflect.ownKeys': 'ownKeys',
'Reflect.preventExtensions': 'preventExtensions',
'Reflect.set': 'set',
'Reflect.setPrototypeOf': 'setPrototypeOf',
// Global functions
'structuredClone': 'structuredClone',
'queueMicrotask': 'queueMicrotask',
'requestIdleCallback': 'requestIdleCallback',
'cancelIdleCallback': 'cancelIdleCallback',
'requestAnimationFrame': 'requestAnimationFrame',
'cancelAnimationFrame': 'cancelAnimationFrame',
'setTimeout': 'setTimeout',
'clearTimeout': 'clearTimeout',
'setInterval': 'setInterval',
'clearInterval': 'clearInterval',
'setImmediate': 'setImmediate',
'clearImmediate': 'clearImmediate',
'fetch': 'fetch',
'BigInt': 'BigInt',
'globalThis': 'globalThis',
'atob': 'atob',
'btoa': 'btoa',
'encodeURI': 'encodeURI',
'decodeURI': 'decodeURI',
'encodeURIComponent': 'encodeURIComponent',
'decodeURIComponent': 'decodeURIComponent',
'escape': 'escape',
'unescape': 'unescape',
'isNaN': 'isNaN',
'isFinite': 'isFinite',
'parseInt': 'parseInt',
'parseFloat': 'parseFloat',
'eval': 'eval',
'Function': 'Function',
'Array': 'Array',
'Object': 'Object',
'String': 'String',
'Number': 'Number',
'Boolean': 'Boolean',
'Date': 'Date',
'RegExp': 'RegExp',
'Error': 'Error',
'TypeError': 'TypeError',
'ReferenceError': 'ReferenceError',
'SyntaxError': 'SyntaxError',
'RangeError': 'RangeError',
'EvalError': 'EvalError',
'URIError': 'URIError',
'Map': 'Map',
'Set': 'Set',
'WeakMap': 'WeakMap',
'WeakSet': 'WeakSet',
'Symbol': 'Symbol',
'Proxy': 'Proxy',
'Reflect': 'Reflect',
'Promise': 'Promise',
'Generator': 'Generator',
'GeneratorFunction': 'GeneratorFunction',
'AsyncFunction': 'AsyncFunction',
'AsyncGenerator': 'AsyncGenerator',
'AsyncGeneratorFunction': 'AsyncGeneratorFunction',
'Intl': 'Intl',
'JSON': 'JSON',
'Math': 'Math',
'console': 'console',
'performance': 'performance',
'crypto': 'crypto',
'location': 'location',
'history': 'history',
'navigator': 'navigator',
'screen': 'screen',
'document': 'document',
'window': 'window',
'global': 'global',
'globalThis': 'globalThis',
'self': 'self',
'top': 'top',
'parent': 'parent',
'frames': 'frames',
'length': 'length',
'name': 'name',
'status': 'status',
'opener': 'opener',
'closed': 'closed',
'innerWidth': 'innerWidth',
'innerHeight': 'innerHeight',
'outerWidth': 'outerWidth',
'outerHeight': 'outerHeight',
'screenX': 'screenX',
'screenY': 'screenY',
'screenLeft': 'screenLeft',
'screenTop': 'screenTop',
'scrollX': 'scrollX',
'scrollY': 'scrollY',
'pageXOffset': 'pageXOffset',
'pageYOffset': 'pageYOffset',
'devicePixelRatio': 'devicePixelRatio',
'orientation': 'orientation',
'onload': 'onload',
'onunload': 'onunload',
'onbeforeunload': 'onbeforeunload',
'onresize': 'onresize',
'onscroll': 'onscroll',
'onfocus': 'onfocus',
'onblur': 'onblur',
'onerror': 'onerror',
'onabort': 'onabort',
'onbeforeprint': 'onbeforeprint',
'onafterprint': 'onafterprint',
'onhashchange': 'onhashchange',
'onlanguagechange': 'onlanguagechange',
'onmessage': 'onmessage',
'onmessageerror': 'onmessageerror',
'onoffline': 'onoffline',
'ononline': 'ononline',
'onpagehide': 'onpagehide',
'onpageshow': 'onpageshow',
'onpopstate': 'onpopstate',
'onrejectionhandled': 'onrejectionhandled',
'onstorage': 'onstorage',
'onunhandledrejection': 'onunhandledrejection',
'onvisibilitychange': 'onvisibilitychange'
};
/**
* Analyze JavaScript content
*/
export function analyzeJSContent(jsContent, options = {}) {
const { requiredLevel = 'low' } = options;
const issues = [];
const foundAPIs = new Set();
try {
const ast = parse(jsContent, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true
});
traverse(ast, {
// Check for .at() usage and other method calls
MemberExpression(path) {
const { object, property } = path.node;
if (property.type === 'Identifier') {
const methodName = property.name;
// Check if it's an Array method we care about
if (JS_APIS[`Array.prototype.${methodName}`]) {
const apiPath = `Array.prototype.${methodName}`;
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
// Check for String methods
if (JS_APIS[`String.prototype.${methodName}`]) {
const apiPath = `String.prototype.${methodName}`;
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
// Check for Promise methods
if (object.type === 'Identifier' && object.name === 'Promise') {
const apiPath = `Promise.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Object methods
if (object.type === 'Identifier' && object.name === 'Object') {
const apiPath = `Object.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Number methods
if (object.type === 'Identifier' && object.name === 'Number') {
const apiPath = `Number.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Math methods
if (object.type === 'Identifier' && object.name === 'Math') {
const apiPath = `Math.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for RegExp methods
if (object.type === 'Identifier' && object.name === 'RegExp') {
const apiPath = `RegExp.prototype.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Map methods
if (object.type === 'Identifier' && object.name === 'Map') {
const apiPath = `Map.prototype.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Set methods
if (object.type === 'Identifier' && object.name === 'Set') {
const apiPath = `Set.prototype.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for WeakMap methods
if (object.type === 'Identifier' && object.name === 'WeakMap') {
const apiPath = `WeakMap.prototype.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for WeakSet methods
if (object.type === 'Identifier' && object.name === 'WeakSet') {
const apiPath = `WeakSet.prototype.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Symbol methods
if (object.type === 'Identifier' && object.name === 'Symbol') {
const apiPath = `Symbol.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Proxy methods
if (object.type === 'Identifier' && object.name === 'Proxy') {
const apiPath = `Proxy.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
// Check for Reflect methods
if (object.type === 'Identifier' && object.name === 'Reflect') {
const apiPath = `Reflect.${methodName}`;
if (JS_APIS[apiPath]) {
if (!foundAPIs.has(apiPath)) {
foundAPIs.add(apiPath);
checkAPI(apiPath, path, issues, requiredLevel);
}
}
}
}
},
// Check for global functions like structuredClone
CallExpression(path) {
const { callee } = path.node;
if (callee.type === 'Identifier') {
const functionName = callee.name;
if (JS_APIS[functionName]) {
if (!foundAPIs.has(functionName)) {
foundAPIs.add(functionName);
checkAPI(functionName, path, issues, requiredLevel);
}
}
}
},
// Check for constructor calls
NewExpression(path) {
const { callee } = path.node;
if (callee.type === 'Identifier') {
const constructorName = callee.name;
// Check for constructor usage
if (JS_APIS[constructorName]) {
if (!foundAPIs.has(constructorName)) {
foundAPIs.add(constructorName);
checkAPI(constructorName, path, issues, requiredLevel);
}
}
}
},
// Check for property access on global objects
Identifier(path) {
const { name } = path.node;
// Check for global object properties
if (JS_APIS[name] && path.isReferencedIdentifier()) {
const parent = path.parent;
// Only check if it's not a method call (handled by MemberExpression)
if (parent.type !== 'MemberExpression' || parent.property !== path.node) {
if (!foundAPIs.has(name)) {
foundAPIs.add(name);
checkAPI(name, path, issues, requiredLevel);
}
}
}
}
});
} catch (error) {
const errorInfo = handleError(error, {
type: 'js_parse',
contentLength: jsContent?.length || 0
});
// Extract line/column from Babel error if available
let line = null, column = null;
if (error.loc) {
line = error.loc.line;
column = error.loc.column;
}
throw new ParseError(
`JavaScript parsing failed: ${error.message}`,
null,
line,
column
);
}
return {
issues,
summary: {
total: issues.length,
errors: issues.filter(i => i.severity === 'error').length,
warnings: issues.filter(i => i.severity === 'warning').length
}
};
}
/**
* Check a JavaScript API
*/
function checkAPI(apiPath, path, issues, requiredLevel) {
const result = checkJavaScriptAPI(apiPath);
const report = generateReport(result, requiredLevel);
// Include all features for baseline scoring (info, warning, error)
const loc = path.node.loc;
issues.push({
line: loc?.start.line,
column: loc?.start.column,
api: apiPath,
severity: report.severity,
message: report.message,
baseline: report.baseline,
support: report.support,
bcdKey: report.bcdKey,
compatible: report.compatible
});
}
/**
* Analyze a JavaScript file
*/
export const analyzeJSFile = safeAsync(async (filePath, options = {}) => {
try {
const content = await readFileWithCleanup(filePath, {
encoding: 'utf-8',
maxSize: options.maxFileSize || 50 * 1024 * 1024
});
const result = analyzeJSContent(content, options);
return {
file: filePath,
...result
};
} catch (error) {
// FileError is already properly formatted by readFileWithCleanup
throw error;
}
}, { operation: 'analyzeJSFile' });
/**
* Format JavaScript issues
*/
export function formatJSIssues(issues) {
return issues.map(issue => {
const location = issue.line ? `${issue.line}:${issue.column}` : 'unknown';
let icon = '';
if (issue.severity === 'error') icon = '❌';
else if (issue.severity === 'warning') icon = '⚠️';
else icon = 'ℹ️';
let supportInfo = '';
if (issue.support) {
const browsers = Object.entries(issue.support)
.slice(0, 3)
.map(([browser, version]) => `${browser} ${version}`)
.join(', ');
supportInfo = `\n Support: ${browsers}`;
}
return ` ${icon} ${location} - ${issue.api}
${issue.message}${supportInfo}`;
}).join('\n\n');
}