Skip to main content
Glama
safety-analyzer.js•13.8 kB
import * as parser from '@babel/parser'; import * as babelTraverse from '@babel/traverse'; // Handle different module formats const traverse = babelTraverse.default || babelTraverse; // Constants for NASA JPL Power of 10 Rules const MAX_FUNCTION_LINES = 60; const MIN_ASSERTIONS_PER_FUNCTION = 2; const MAX_NESTING_DEPTH = 3; const CRITICAL_PENALTY = 20; const WARNING_PENALTY = 5; const INFO_PENALTY = 1; const MAX_SCORE = 100; // NASA JPL Power of 10 Safety Rules Analyzer for JavaScript export class SafetyAnalyzer { constructor() { // Assert: Constructor must initialize all properties if (!this) { throw new TypeError('Invalid constructor context'); } this.violations = []; this.currentFunction = null; this.functionCallStack = []; this.globalScope = new Set(); this.ignoredReturns = []; this.filename = ''; } analyze(code, filename = 'code.js') { // Assert: Input validation if (typeof code !== 'string') { throw new TypeError('Code must be a string'); } if (typeof filename !== 'string') { throw new TypeError('Filename must be a string'); } this.violations = []; this.currentFunction = null; this.functionCallStack = []; this.globalScope.clear(); this.ignoredReturns = []; this.filename = filename; try { // Parse code into AST const ast = parser.parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx'], errorRecovery: true, }); // Traverse AST and check for violations traverse(ast, { // Track function boundaries FunctionDeclaration: { enter: (path) => this.enterFunction(path), exit: (path) => this.exitFunction(path), }, FunctionExpression: { enter: (path) => this.enterFunction(path), exit: (path) => this.exitFunction(path), }, ArrowFunctionExpression: { enter: (path) => this.enterFunction(path), exit: (path) => this.exitFunction(path), }, // Check for recursion CallExpression: (path) => this.checkRecursion(path), // Check loops WhileStatement: (path) => this.checkLoopBounds(path, 'while'), ForStatement: (path) => this.checkLoopBounds(path, 'for'), DoWhileStatement: (path) => this.checkLoopBounds(path, 'do-while'), ForInStatement: (path) => this.checkLoopBounds(path, 'for-in'), ForOfStatement: (path) => this.checkLoopBounds(path, 'for-of'), // Check variable scope VariableDeclaration: (path) => this.checkVariableScope(path), // Check assertions IfStatement: (path) => this.checkAssertions(path), ConditionalExpression: (path) => this.checkAssertions(path), // Check return value usage ExpressionStatement: (path) => this.checkReturnValueUsage(path), // Check nesting depth MemberExpression: (path) => this.checkNestingDepth(path), }); return this.generateReport(); } catch (error) { return { success: false, error: `Failed to analyze ${this.filename}: ${error.message}`, violations: [], }; } } enterFunction(path) { // Assert: Path must be valid if (!path || !path.node) { throw new Error('Invalid function path'); } const node = path.node; const name = node.id?.name || '<anonymous>'; const start = node.loc?.start || { line: 0 }; const end = node.loc?.end || { line: 0 }; this.currentFunction = { name, node, path, startLine: start.line, endLine: end.line, assertions: 0, hasReturn: node.type === 'ArrowFunctionExpression' && node.expression, }; // Check function length const lines = this.currentFunction.endLine - this.currentFunction.startLine + 1; if (lines > MAX_FUNCTION_LINES) { this.addViolation( 'function-length', `Function '${name}' is ${lines} lines long (max: ${MAX_FUNCTION_LINES} lines)`, start.line, 'critical', ); } this.functionCallStack.push(name); } exitFunction(path) { // Assert: Must have current function context if (!this.currentFunction) { return; } // Check assertion count if (this.currentFunction.assertions < MIN_ASSERTIONS_PER_FUNCTION) { this.addViolation( 'insufficient-assertions', `Function '${this.currentFunction.name}' has only ${this.currentFunction.assertions} assertion(s) (min: ${MIN_ASSERTIONS_PER_FUNCTION})`, this.currentFunction.startLine, 'warning', ); } this.functionCallStack.pop(); this.currentFunction = null; } checkRecursion(path) { // Assert: Path must be valid if (!path || !path.node) { return; } const callee = path.node.callee; // Direct recursion check if (callee.type === 'Identifier' && this.functionCallStack.includes(callee.name)) { this.addViolation( 'recursion', `Recursive call detected in function '${callee.name}'`, path.node.loc?.start.line, 'critical', ); } // Check for indirect recursion patterns this.checkIndirectRecursion(path); } checkIndirectRecursion(path) { // Assert: Path must be valid if (!path || !path.node) { return; } // Check for setTimeout/setInterval with self-reference const callee = path.node.callee; if (callee.type === 'Identifier' && (callee.name === 'setTimeout' || callee.name === 'setInterval')) { const firstArg = path.node.arguments[0]; if (firstArg && this.currentFunction && firstArg.type === 'Identifier' && firstArg.name === this.currentFunction.name) { this.addViolation( 'indirect-recursion', `Potential indirect recursion via ${callee.name}`, path.node.loc?.start.line, 'warning', ); } } } checkLoopBounds(path, loopType) { // Assert: Valid inputs if (!path || !path.node || typeof loopType !== 'string') { return; } const node = path.node; let hasFixedBound = false; switch (loopType) { case 'for': hasFixedBound = this.checkForLoopBounds(node); break; case 'while': case 'do-while': hasFixedBound = this.checkWhileLoopBounds(node, loopType); break; case 'for-in': case 'for-of': // These iterate over collections, generally safe hasFixedBound = true; break; } if (!hasFixedBound && loopType === 'for') { this.addViolation( 'unbounded-loop', 'Loop may not have fixed bounds - ensure termination condition', node.loc?.start.line, 'warning', ); } } checkForLoopBounds(node) { // Assert: Node must be valid if (!node || !node.test) { return false; } // Check if for loop has a numeric limit if (node.test.type === 'BinaryExpression') { const { right, operator } = node.test; if (['<', '<=', '>', '>='].includes(operator) && (right.type === 'NumericLiteral' || right.type === 'Literal')) { return true; } } return false; } checkWhileLoopBounds(node, loopType) { // Assert: Node must be valid if (!node || !node.test) { return false; } // Check for literal true/false if (node.test.type === 'BooleanLiteral' && node.test.value === true) { this.addViolation( 'infinite-loop', `Infinite ${loopType} loop detected (while(true))`, node.loc?.start.line, 'critical', ); return false; } // Harder to detect fixed bounds in while loops return false; } checkVariableScope(path) { // Assert: Path must be valid if (!path || !path.node) { return; } const node = path.node; // Check if variable is in global scope try { const isGlobal = path.scope.block.type === 'Program' || (path.scope.parent && path.scope.parent.block.type === 'Program'); if (isGlobal) { node.declarations.forEach(decl => { if (decl.id.type === 'Identifier') { this.globalScope.add(decl.id.name); this.addViolation( 'global-variable', `Global variable '${decl.id.name}' - restrict scope to smallest possible`, node.loc?.start.line, 'warning', ); } }); } } catch (error) { // Skip if scope analysis fails - error is expected in some contexts } // Check for var usage (broader scope than let/const) if (node.kind === 'var') { this.addViolation( 'var-usage', 'Use of \'var\' - prefer \'const\' or \'let\' for better scope control', node.loc?.start.line, 'warning', ); } } checkAssertions(path) { // Assert: Must have valid context if (!this.currentFunction || !path || !path.node) { return; } const node = path.node; // Count validation checks as assertions if (node.type === 'IfStatement') { const test = node.test; // Look for validation patterns if (this.isValidationCheck(test)) { this.currentFunction.assertions++; } } } isValidationCheck(node) { // Assert: Node must exist if (!node) { return false; } // Direct validation: if (!param) throw if (node.type === 'UnaryExpression' && node.operator === '!') { return true; } // Comparison validation: if (param === null) if (node.type === 'BinaryExpression' && ['===', '!==', '==', '!=', '<', '>', '<=', '>='].includes(node.operator)) { return true; } // typeof checks: if (typeof param === 'string') if (node.type === 'BinaryExpression' && node.left.type === 'UnaryExpression' && node.left.operator === 'typeof') { return true; } return false; } checkReturnValueUsage(path) { // Assert: Path must be valid if (!path || !path.node) { return; } const node = path.node; // Check if expression result is ignored if (node.expression && node.expression.type === 'CallExpression') { // Check for ignored Promise/async returns if (this.isAsyncCall(node.expression)) { this.addViolation( 'ignored-async-return', 'Async function return value ignored - handle Promise or use await', node.loc?.start.line, 'warning', ); } } } isAsyncCall(node) { // Assert: Node must be valid if (!node || !node.callee) { return false; } // Simple heuristic: function names that suggest async behavior if (node.callee.type === 'Identifier') { const name = node.callee.name; return name.includes('fetch') || name.includes('async') || name.endsWith('Async') || name.startsWith('get') || name.startsWith('load'); } // Method calls that are commonly async if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier') { const method = node.callee.property.name; return ['then', 'catch', 'finally'].includes(method); } return false; } checkNestingDepth(path) { // Assert: Path must be valid if (!path || !path.node) { return; } const node = path.node; let depth = 0; let current = node; // Count chained member expressions while (current && current.type === 'MemberExpression') { depth++; current = current.object; } if (depth > MAX_NESTING_DEPTH) { this.addViolation( 'deep-nesting', `Deep property access chain (${depth} levels) - limit to single dereference`, node.loc?.start.line, 'info', ); } } addViolation(rule, message, line, severity = 'warning') { // Assert: Valid inputs if (typeof rule !== 'string' || typeof message !== 'string') { throw new TypeError('Invalid violation parameters'); } if (!['critical', 'warning', 'info'].includes(severity)) { throw new TypeError('Invalid severity level'); } this.violations.push({ rule, message, line: line || 0, severity, filename: this.filename, }); } generateReport() { // Assert: Violations must be an array if (!Array.isArray(this.violations)) { throw new TypeError('Invalid violations state'); } const criticalCount = this.violations.filter(v => v.severity === 'critical').length; const warningCount = this.violations.filter(v => v.severity === 'warning').length; const infoCount = this.violations.filter(v => v.severity === 'info').length; // Calculate safety score (0-100) const score = Math.max( 0, MAX_SCORE - (criticalCount * CRITICAL_PENALTY) - (warningCount * WARNING_PENALTY) - (infoCount * INFO_PENALTY), ); return { success: true, score, filename: this.filename, summary: { total: this.violations.length, critical: criticalCount, warning: warningCount, info: infoCount, }, violations: this.violations.sort((a, b) => { // Sort by severity then line number const severityOrder = { critical: 0, warning: 1, info: 2 }; if (severityOrder[a.severity] !== severityOrder[b.severity]) { return severityOrder[a.severity] - severityOrder[b.severity]; } return a.line - b.line; }), }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/moikas-code/moidvk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server