diff --git a/.tektonlintrc.yaml b/.tektonlintrc.yaml index 087a332..d2c67f3 100644 --- a/.tektonlintrc.yaml +++ b/.tektonlintrc.yaml @@ -17,6 +17,7 @@ rules: # error | warning | off no-latest-image: warning prefer-beta: warning prefer-kebab-case: warning + prefer-camel-kebab-case: off no-unused-param: warning no-missing-resource: error no-undefined-param: error diff --git a/src/default-rule-config.ts b/src/default-rule-config.ts index d51ec5c..9186f8e 100644 --- a/src/default-rule-config.ts +++ b/src/default-rule-config.ts @@ -19,6 +19,7 @@ const defaultRules: RulesConfig = { 'no-latest-image': 'warning', 'prefer-beta': 'warning', 'prefer-kebab-case': 'warning', + 'prefer-camel-kebab-case': 'off', 'no-unused-param': 'warning', 'no-missing-resource': 'error', 'no-undefined-param': 'error', diff --git a/src/rule-loader.ts b/src/rule-loader.ts index add736c..4688e32 100644 --- a/src/rule-loader.ts +++ b/src/rule-loader.ts @@ -55,6 +55,9 @@ const defaultRules = { // prefer-kebab-case 'prefer-kebab-case': (await import('./rules/prefer-kebab-case.js')).default, + // prefer-camel-kebab-case + 'prefer-camel-kebab-case': (await import('./rules/prefer-camel-kebab-case.js')).default, + // prefer-when-expression 'prefer-when-expression': (await import('./rules/prefer-when-expression.js')).default, diff --git a/src/rules/prefer-camel-kebab-case.ts b/src/rules/prefer-camel-kebab-case.ts new file mode 100644 index 0000000..474748e --- /dev/null +++ b/src/rules/prefer-camel-kebab-case.ts @@ -0,0 +1,65 @@ +import { walk, pathToString } from '../walk.js'; + +const isValidKebabName = (name) => { + const valid = new RegExp('^[a-z0-9-()$.]*$'); + return valid.test(name); +}; + +const isValidCamelName = (name) => { + const valid = new RegExp('^[a-z_][a-z0-9A-Z()$.]*$'); + return valid.test(name); +}; + +const isValidName = (name) => { + return isValidKebabName(name) || isValidCamelName(name); +}; + +const naming = (resource, prefix, report) => (node, path, parent) => { + let name = node; + const isNameDefinition = /.name$/.test(path); + + if (path.includes('env') && path.includes('name')) return; + + if (isNameDefinition && !isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + 'name', + ); + return; + } + + const parameterPlacementRx = new RegExp(`\\$\\(${prefix}.(.*?)\\)`); + const m = node && node.toString().match(parameterPlacementRx); + + if (m) { + name = m[1]; + if (!isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + path[path.length - 1], + ); + } + } +}; + +export default (docs, tekton, report) => { + for (const pipeline of Object.values(tekton.pipelines)) { + walk(pipeline.spec.tasks, ['spec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk(pipeline.spec.finally, ['spec', 'finally'], naming(pipeline.metadata.name, 'params', report)); + } + + for (const pipeline of Object.values(tekton.pipelineRuns)) { + walk(pipeline.spec.tasks, ['spec', 'pipelineSpec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk( + pipeline.spec.finally, + ['spec', 'pipelineSpec', 'finally'], + naming(pipeline.metadata.name, 'params', report), + ); + } +};