yaml.js•17.6 kB
/**
* YAML/Configuration language handler for the Code-Map Generator tool.
* This file contains the language handler for YAML and other configuration files.
*/
import { BaseLanguageHandler } from './base.js';
import { getNodeText } from '../astAnalyzer.js';
import logger from '../../../logger.js';
import path from 'path';
/**
* Language handler for YAML/Configuration.
* Provides enhanced function name detection for YAML and other configuration files.
*/
export class YamlHandler extends BaseLanguageHandler {
/**
* Options for the handler.
*/
options;
/**
* Gets the query patterns for function detection.
*/
getFunctionQueryPatterns() {
return [
'block_mapping_pair',
'flow_mapping',
'block_sequence_item'
];
}
/**
* Gets the query patterns for class detection.
*/
getClassQueryPatterns() {
return [
'document',
'block_mapping',
'flow_mapping'
];
}
/**
* Gets the query patterns for import detection.
*/
getImportQueryPatterns() {
return [
'block_mapping_pair'
];
}
/**
* Extracts the function name from an AST node.
*/
extractFunctionName(node, sourceCode, options) {
try {
// Handle block mapping pairs (key: value)
if (node.type === 'block_mapping_pair') {
const keyNode = node.childForFieldName('key');
if (keyNode) {
const key = getNodeText(keyNode, sourceCode);
// Check for common function-like keys
if (['run', 'script', 'command', 'exec', 'test', 'build', 'deploy'].includes(key)) {
return `${key}_command`;
}
// Check for CI/CD steps
if (this.isInCiCdContext(node, sourceCode)) {
return `step_${key}`;
}
// Check for Kubernetes resources
if (this.isKubernetesResource(node, sourceCode)) {
const valueNode = node.childForFieldName('value');
if (valueNode) {
const nameNode = this.findNameInMapping(valueNode);
if (nameNode) {
return `${key}_${getNodeText(nameNode, sourceCode)}`;
}
}
return `${key}_resource`;
}
return key;
}
}
// Handle block sequence items (- item)
if (node.type === 'block_sequence_item') {
// Check if this is a step in a sequence
if (this.isInCiCdContext(node, sourceCode)) {
// Try to find a name or key property
const valueNode = node.childForFieldName('value');
if (valueNode) {
if (valueNode.type === 'block_mapping') {
const nameNode = this.findNameInMapping(valueNode);
if (nameNode) {
return `step_${getNodeText(nameNode, sourceCode)}`;
}
}
}
return 'sequence_step';
}
return 'sequence_item';
}
return 'configuration_item';
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error extracting YAML/Configuration function name');
return 'configuration_item';
}
}
/**
* Checks if a node is in a CI/CD context.
*/
isInCiCdContext(node, sourceCode) {
try {
// Check if we're in a CI/CD file based on filename
if (this.options?.filePath) {
const filename = path.basename(this.options.filePath).toLowerCase();
if (filename.includes('workflow') ||
filename.includes('pipeline') ||
filename.includes('ci') ||
filename.includes('cd') ||
filename.includes('travis') ||
filename.includes('jenkins') ||
filename.includes('gitlab') ||
filename.includes('github') ||
filename.includes('azure-pipelines')) {
return true;
}
}
// Check for CI/CD keywords in the document
let current = node;
while (current.parent) {
current = current.parent;
if (current.type === 'block_mapping') {
for (let i = 0; i < current.childCount; i++) {
const child = current.child(i);
if (child?.type === 'block_mapping_pair') {
const keyNode = child.childForFieldName('key');
if (keyNode) {
const key = getNodeText(keyNode, sourceCode);
if (['jobs', 'stages', 'steps', 'tasks', 'pipeline', 'workflow'].includes(key)) {
return true;
}
}
}
}
}
}
return false;
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error checking if YAML node is in CI/CD context');
return false;
}
}
/**
* Checks if a node is a Kubernetes resource.
*/
isKubernetesResource(node, sourceCode) {
try {
// Check if we're in a Kubernetes file based on filename
if (this.options?.filePath) {
const filename = path.basename(this.options.filePath).toLowerCase();
if (filename.includes('kube') ||
filename.includes('k8s') ||
filename.endsWith('.yaml') ||
filename.endsWith('.yml')) {
// Look for kind and apiVersion fields
let current = node;
while (current.parent) {
current = current.parent;
if (current.type === 'block_mapping') {
let hasKind = false;
let hasApiVersion = false;
for (let i = 0; i < current.childCount; i++) {
const child = current.child(i);
if (child?.type === 'block_mapping_pair') {
const keyNode = child.childForFieldName('key');
if (keyNode) {
const key = getNodeText(keyNode, sourceCode);
if (key === 'kind')
hasKind = true;
if (key === 'apiVersion')
hasApiVersion = true;
}
}
}
if (hasKind && hasApiVersion) {
return true;
}
}
}
}
}
return false;
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error checking if YAML node is a Kubernetes resource');
return false;
}
}
/**
* Finds a name property in a mapping.
*/
findNameInMapping(node) {
try {
if (node.type === 'block_mapping') {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'block_mapping_pair') {
const keyNode = child.childForFieldName('key');
if (keyNode && ['name', 'id', 'key', 'title'].includes(keyNode.text)) {
return child.childForFieldName('value');
}
}
}
}
return null;
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error finding name in YAML mapping');
return null;
}
}
/**
* Extracts the class name from an AST node.
*/
extractClassName(node, sourceCode) {
try {
if (node.type === 'document') {
// For Kubernetes resources, use kind as the class name
const contentNode = node.childForFieldName('content');
if (contentNode?.type === 'block_mapping') {
const mapping = contentNode;
let kind = null;
for (let i = 0; i < mapping.childCount; i++) {
const child = mapping.child(i);
if (child?.type === 'block_mapping_pair') {
const keyNode = child.childForFieldName('key');
if (keyNode && getNodeText(keyNode, sourceCode) === 'kind') {
const valueNode = child.childForFieldName('value');
if (valueNode) {
kind = getNodeText(valueNode, sourceCode);
break;
}
}
}
}
if (kind) {
return `K8s_${kind}`;
}
}
// For CI/CD files, use the filename as the class name
if (this.options?.filePath) {
const filename = path.basename(this.options.filePath, path.extname(this.options.filePath));
if (filename.includes('workflow') ||
filename.includes('pipeline') ||
filename.includes('ci') ||
filename.includes('cd')) {
return `CI_${filename}`;
}
}
// Default to the filename without extension
if (this.options?.filePath) {
return path.basename(this.options.filePath, path.extname(this.options.filePath));
}
}
else if (node.type === 'block_mapping' || node.type === 'flow_mapping') {
// Try to find a name or kind property
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'block_mapping_pair') {
const keyNode = child.childForFieldName('key');
if (keyNode) {
const key = getNodeText(keyNode, sourceCode);
if (key === 'kind') {
const valueNode = child.childForFieldName('value');
if (valueNode) {
return getNodeText(valueNode, sourceCode);
}
}
else if (key === 'name') {
const valueNode = child.childForFieldName('value');
if (valueNode) {
return `Config_${getNodeText(valueNode, sourceCode)}`;
}
}
}
}
}
}
return 'Configuration';
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error extracting YAML/Configuration class name');
return 'Configuration';
}
}
/**
* Extracts the import path from an AST node.
*/
extractImportPath(node, sourceCode) {
try {
// Handle imports in YAML (e.g., include, import, $ref)
if (node.type === 'block_mapping_pair') {
const keyNode = node.childForFieldName('key');
if (keyNode) {
const key = getNodeText(keyNode, sourceCode);
if (key === 'include' || key === 'import' || key === '$ref' || key === 'extends') {
const valueNode = node.childForFieldName('value');
if (valueNode) {
return getNodeText(valueNode, sourceCode);
}
}
}
}
return 'unknown';
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error extracting YAML/Configuration import path');
return 'unknown';
}
}
/**
* Extracts the function comment from an AST node.
*/
extractFunctionComment(node, sourceCode) {
try {
// Look for comments before the node
const current = node;
let prev = current.previousNamedSibling;
while (prev && prev.type !== 'comment') {
prev = prev.previousNamedSibling;
}
if (prev && prev.type === 'comment') {
// Extract the comment text
const commentText = getNodeText(prev, sourceCode);
// Remove comment markers and whitespace
return commentText
.replace(/^#\s*/mg, '')
.trim();
}
return undefined;
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error extracting YAML/Configuration function comment');
return undefined;
}
}
/**
* Extracts the class comment from an AST node.
*/
extractClassComment(node, sourceCode) {
try {
// Look for comments at the beginning of the document
if (node.type === 'document') {
let firstChild = node.firstChild;
// Skip directives
while (firstChild && firstChild.type === 'directive') {
firstChild = firstChild.nextNamedSibling;
}
// Check if the first non-directive node is a comment
if (firstChild && firstChild.type === 'comment') {
// Extract the comment text
const commentText = getNodeText(firstChild, sourceCode);
// Remove comment markers and whitespace
return commentText
.replace(/^#\s*/mg, '')
.trim();
}
}
else {
// Look for comments before the node
const current = node;
let prev = current.previousNamedSibling;
while (prev && prev.type !== 'comment') {
prev = prev.previousNamedSibling;
}
if (prev && prev.type === 'comment') {
// Extract the comment text
const commentText = getNodeText(prev, sourceCode);
// Remove comment markers and whitespace
return commentText
.replace(/^#\s*/mg, '')
.trim();
}
}
return undefined;
}
catch (error) {
logger.warn({ err: error, nodeType: node.type }, 'Error extracting YAML/Configuration class comment');
return undefined;
}
}
/**
* Detects the framework used in the source code.
*/
detectFramework(sourceCode) {
try {
// Kubernetes detection
if (sourceCode.includes('apiVersion:') &&
sourceCode.includes('kind:') &&
(sourceCode.includes('metadata:') || sourceCode.includes('spec:'))) {
return 'kubernetes';
}
// GitHub Actions detection
if (sourceCode.includes('name:') &&
sourceCode.includes('on:') &&
sourceCode.includes('jobs:')) {
return 'github-actions';
}
// Docker Compose detection
if (sourceCode.includes('version:') &&
sourceCode.includes('services:') &&
(sourceCode.includes('image:') || sourceCode.includes('build:'))) {
return 'docker-compose';
}
// GitLab CI detection
if (sourceCode.includes('stages:') &&
(sourceCode.includes('script:') || sourceCode.includes('image:'))) {
return 'gitlab-ci';
}
// Travis CI detection
if (sourceCode.includes('language:') &&
(sourceCode.includes('script:') || sourceCode.includes('before_script:'))) {
return 'travis-ci';
}
return null;
}
catch (error) {
logger.warn({ err: error }, 'Error detecting YAML/Configuration framework');
return null;
}
}
}