Skip to content

Commit

Permalink
feat: move rules options to a separate property for better extensibility
Browse files Browse the repository at this point in the history
  • Loading branch information
zavoloklom committed Dec 15, 2024
1 parent b80549d commit 929123a
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 102 deletions.
1 change: 1 addition & 0 deletions src/linter/linter.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface LintRule {
category: LintRuleCategory; // Category under which this rule falls
severity: LintRuleSeverity; // Default severity level for this rule
fixable: boolean; // Is it possible to fix this
options?: object; // Configurable options for this rule

// Method for generating an error message if the rule is violated
getMessage(details?: object): string;
Expand Down
17 changes: 12 additions & 5 deletions src/rules/no-build-and-image-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import type {
} from '../linter/linter.types';
import { findLineNumberForService } from '../util/line-finder';

interface NoBuildAndImageRuleOptions {
export interface NoBuildAndImageRuleInputOptions {
checkPullPolicy?: boolean;
}

interface NoBuildAndImageRuleOptions {
checkPullPolicy: boolean;
}

export default class NoBuildAndImageRule implements LintRule {
public name = 'no-build-and-image';

Expand All @@ -31,10 +35,13 @@ export default class NoBuildAndImageRule implements LintRule {

public fixable: boolean = false;

private readonly checkPullPolicy: boolean;
public options: NoBuildAndImageRuleOptions;

constructor(options?: NoBuildAndImageRuleOptions) {
this.checkPullPolicy = options?.checkPullPolicy ?? true;
constructor(options?: NoBuildAndImageRuleInputOptions) {
const defaultOptions: NoBuildAndImageRuleOptions = {
checkPullPolicy: true,
};
this.options = { ...defaultOptions, ...options };
}

// eslint-disable-next-line class-methods-use-this
Expand All @@ -61,7 +68,7 @@ export default class NoBuildAndImageRule implements LintRule {
const hasImage = service.has('image');
const hasPullPolicy = service.has('pull_policy');

if (hasBuild && hasImage && (!this.checkPullPolicy || !hasPullPolicy)) {
if (hasBuild && hasImage && (!this.options.checkPullPolicy || !hasPullPolicy)) {
const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'build');
errors.push({
rule: this.name,
Expand Down
30 changes: 18 additions & 12 deletions src/rules/require-quotes-in-ports-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import type {
} from '../linter/linter.types';
import { findLineNumberForService } from '../util/line-finder';

export interface RequireQuotesInPortsRuleInputOptions {
quoteType?: 'single' | 'double';
}

interface RequireQuotesInPortsRuleOptions {
quoteType: 'single' | 'double';
portsSections: string[];
}

export default class RequireQuotesInPortsRule implements LintRule {
Expand All @@ -30,22 +35,23 @@ export default class RequireQuotesInPortsRule implements LintRule {

public fixable: boolean = true;

public options: RequireQuotesInPortsRuleOptions;

constructor(options?: RequireQuotesInPortsRuleInputOptions) {
const defaultOptions: RequireQuotesInPortsRuleOptions = {
quoteType: 'single',
portsSections: ['ports', 'expose'],
};
this.options = { ...defaultOptions, ...options };
}

// eslint-disable-next-line class-methods-use-this
public getMessage(): string {
return 'Ports in `ports` and `expose` sections should be enclosed in quotes.';
}

private readonly quoteType: 'single' | 'double';

private readonly portsSections: string[];

constructor(options?: RequireQuotesInPortsRuleOptions) {
this.quoteType = options?.quoteType || 'single';
this.portsSections = ['ports', 'expose'];
}

private getQuoteType(): Scalar.Type {
return this.quoteType === 'single' ? 'QUOTE_SINGLE' : 'QUOTE_DOUBLE';
return this.options.quoteType === 'single' ? 'QUOTE_SINGLE' : 'QUOTE_DOUBLE';
}

// Static method to extract and process values
Expand Down Expand Up @@ -79,7 +85,7 @@ export default class RequireQuotesInPortsRule implements LintRule {
const errors: LintMessage[] = [];
const parsedDocument = parseDocument(context.sourceCode);

this.portsSections.forEach((section) => {
this.options.portsSections.forEach((section) => {
RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => {
if (port.type !== this.getQuoteType()) {
errors.push({
Expand Down Expand Up @@ -109,7 +115,7 @@ export default class RequireQuotesInPortsRule implements LintRule {
public fix(content: string): string {
const parsedDocument = parseDocument(content);

this.portsSections.forEach((section) => {
this.options.portsSections.forEach((section) => {
RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => {
if (port.type !== this.getQuoteType()) {
const newPort = new Scalar(String(port.value));
Expand Down
33 changes: 15 additions & 18 deletions src/rules/service-image-require-explicit-tag-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import type {
} from '../linter/linter.types';
import { findLineNumberForService } from '../util/line-finder';

interface ServiceImageRequireExplicitTagRuleOptions {
export interface ServiceImageRequireExplicitTagRuleInputOptions {
prohibitedTags?: string[];
}

interface ServiceImageRequireExplicitTagRuleOptions {
prohibitedTags: string[];
}

export default class ServiceImageRequireExplicitTagRule implements LintRule {
public name = 'service-image-require-explicit-tag';

Expand All @@ -31,33 +35,26 @@ export default class ServiceImageRequireExplicitTagRule implements LintRule {

public fixable: boolean = false;

public options: ServiceImageRequireExplicitTagRuleOptions;

constructor(options?: ServiceImageRequireExplicitTagRuleInputOptions) {
const defaultOptions: ServiceImageRequireExplicitTagRuleOptions = {
prohibitedTags: ['latest', 'stable', 'edge', 'test', 'nightly', 'dev', 'beta', 'canary'],
};
this.options = { ...defaultOptions, ...options };
}

// eslint-disable-next-line class-methods-use-this
public getMessage({ serviceName, image }: { serviceName: string; image: string }): string {
return `Service "${serviceName}" is using the image "${image}", which does not have a concrete version tag. Specify a concrete version tag.`;
}

private readonly prohibitedTags: string[];

constructor(options?: ServiceImageRequireExplicitTagRuleOptions) {
// Default prohibited tags if not provided
this.prohibitedTags = options?.prohibitedTags || [
'latest',
'stable',
'edge',
'test',
'nightly',
'dev',
'beta',
'canary',
];
}

private isImageTagExplicit(image: string): boolean {
const lastPart = image.split('/').pop();
if (!lastPart || !lastPart.includes(':')) return false;

const [, tag] = lastPart.split(':');
return !this.prohibitedTags.includes(tag);
return !this.options.prohibitedTags.includes(tag);
}

public check(context: LintContext): LintMessage[] {
Expand Down
89 changes: 44 additions & 45 deletions src/rules/service-keys-order-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import type {
} from '../linter/linter.types';
import { findLineNumberForService } from '../util/line-finder';

interface ServiceKeysOrderRuleOptions {
export interface ServiceKeysOrderRuleInputOptions {
groupOrder?: GroupOrderEnum[];
groups?: Partial<Record<GroupOrderEnum, string[]>>;
}

enum GroupOrderEnum {
interface ServiceKeysOrderRuleOptions {
groupOrder: GroupOrderEnum[];
groups: Record<GroupOrderEnum, string[]>;
}

export enum GroupOrderEnum {
CoreDefinitions = 'Core Definitions',
ServiceDependencies = 'Service Dependencies',
DataManagementAndConfiguration = 'Data Management and Configuration',
Expand All @@ -27,31 +32,6 @@ enum GroupOrderEnum {
Other = 'Other',
}

// Default group order and groups
const defaultGroupOrder: GroupOrderEnum[] = [
GroupOrderEnum.CoreDefinitions,
GroupOrderEnum.ServiceDependencies,
GroupOrderEnum.DataManagementAndConfiguration,
GroupOrderEnum.EnvironmentConfiguration,
GroupOrderEnum.Networking,
GroupOrderEnum.RuntimeBehavior,
GroupOrderEnum.OperationalMetadata,
GroupOrderEnum.SecurityAndExecutionContext,
GroupOrderEnum.Other,
];

const defaultGroups: Record<GroupOrderEnum, string[]> = {
[GroupOrderEnum.CoreDefinitions]: ['image', 'build', 'container_name'],
[GroupOrderEnum.ServiceDependencies]: ['depends_on'],
[GroupOrderEnum.DataManagementAndConfiguration]: ['volumes', 'volumes_from', 'configs', 'secrets'],
[GroupOrderEnum.EnvironmentConfiguration]: ['environment', 'env_file'],
[GroupOrderEnum.Networking]: ['ports', 'networks', 'network_mode', 'extra_hosts'],
[GroupOrderEnum.RuntimeBehavior]: ['command', 'entrypoint', 'working_dir', 'restart', 'healthcheck'],
[GroupOrderEnum.OperationalMetadata]: ['logging', 'labels'],
[GroupOrderEnum.SecurityAndExecutionContext]: ['user', 'isolation'],
[GroupOrderEnum.Other]: [],
};

export default class ServiceKeysOrderRule implements LintRule {
public name = 'service-keys-order';

Expand Down Expand Up @@ -83,28 +63,47 @@ export default class ServiceKeysOrderRule implements LintRule {
return `Key "${key}" in service "${serviceName}" is out of order. Expected order is: ${correctOrder.join(', ')}.`;
}

private readonly groupOrder: GroupOrderEnum[];

private readonly groups: Record<GroupOrderEnum, string[]>;

constructor(options?: ServiceKeysOrderRuleOptions) {
this.groupOrder = options?.groupOrder?.length ? options.groupOrder : defaultGroupOrder;

this.groups = { ...defaultGroups };
if (options?.groups) {
Object.keys(options.groups).forEach((group) => {
const groupKey = group as GroupOrderEnum;
if (defaultGroups[groupKey] && options.groups) {
this.groups[groupKey] = options.groups[groupKey]!;
}
});
}
public options: ServiceKeysOrderRuleOptions;

constructor(options?: ServiceKeysOrderRuleInputOptions) {
const defaultOptions: ServiceKeysOrderRuleOptions = {
groups: {
[GroupOrderEnum.CoreDefinitions]: ['image', 'build', 'container_name'],
[GroupOrderEnum.ServiceDependencies]: ['depends_on'],
[GroupOrderEnum.DataManagementAndConfiguration]: ['volumes', 'volumes_from', 'configs', 'secrets'],
[GroupOrderEnum.EnvironmentConfiguration]: ['environment', 'env_file'],
[GroupOrderEnum.Networking]: ['ports', 'networks', 'network_mode', 'extra_hosts'],
[GroupOrderEnum.RuntimeBehavior]: ['command', 'entrypoint', 'working_dir', 'restart', 'healthcheck'],
[GroupOrderEnum.OperationalMetadata]: ['logging', 'labels'],
[GroupOrderEnum.SecurityAndExecutionContext]: ['user', 'isolation'],
[GroupOrderEnum.Other]: [],
},
groupOrder: [
GroupOrderEnum.CoreDefinitions,
GroupOrderEnum.ServiceDependencies,
GroupOrderEnum.DataManagementAndConfiguration,
GroupOrderEnum.EnvironmentConfiguration,
GroupOrderEnum.Networking,
GroupOrderEnum.RuntimeBehavior,
GroupOrderEnum.OperationalMetadata,
GroupOrderEnum.SecurityAndExecutionContext,
GroupOrderEnum.Other,
],
};

this.options = {
groups: {
...defaultOptions.groups,
...options?.groups,
},
groupOrder: options?.groupOrder || defaultOptions.groupOrder,
};
}

private getCorrectOrder(keys: string[]): string[] {
const otherKeys = keys.filter((key) => !Object.values(this.groups).flat().includes(key)).sort();
const otherKeys = keys.filter((key) => !Object.values(this.options.groups).flat().includes(key)).sort();

return [...this.groupOrder.flatMap((group) => this.groups[group]), ...otherKeys];
return [...this.options.groupOrder.flatMap((group) => this.options.groups[group]), ...otherKeys];
}

public check(context: LintContext): LintMessage[] {
Expand Down
47 changes: 26 additions & 21 deletions src/rules/top-level-properties-order-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import type {
} from '../linter/linter.types';
import { findLineNumberByKey } from '../util/line-finder';

interface TopLevelPropertiesOrderRuleOptions {
export interface TopLevelPropertiesOrderRuleInputOptions {
customOrder?: TopLevelKeys[];
}

interface TopLevelPropertiesOrderRuleOptions {
customOrder: TopLevelKeys[];
}

export enum TopLevelKeys {
XProperties = 'x-properties',
Version = 'version',
Expand All @@ -26,18 +30,6 @@ export enum TopLevelKeys {
Configs = 'configs',
}

export const DEFAULT_ORDER: TopLevelKeys[] = [
TopLevelKeys.XProperties,
TopLevelKeys.Version,
TopLevelKeys.Name,
TopLevelKeys.Include,
TopLevelKeys.Services,
TopLevelKeys.Networks,
TopLevelKeys.Volumes,
TopLevelKeys.Secrets,
TopLevelKeys.Configs,
];

export default class TopLevelPropertiesOrderRule implements LintRule {
public name = 'top-level-properties-order';

Expand All @@ -56,17 +48,30 @@ export default class TopLevelPropertiesOrderRule implements LintRule {

public fixable: boolean = true;

public options: TopLevelPropertiesOrderRuleOptions;

constructor(options?: TopLevelPropertiesOrderRuleInputOptions) {
const defaultOptions: TopLevelPropertiesOrderRuleOptions = {
customOrder: [
TopLevelKeys.XProperties,
TopLevelKeys.Version,
TopLevelKeys.Name,
TopLevelKeys.Include,
TopLevelKeys.Services,
TopLevelKeys.Networks,
TopLevelKeys.Volumes,
TopLevelKeys.Secrets,
TopLevelKeys.Configs,
],
};
this.options = { ...defaultOptions, ...options };
}

// eslint-disable-next-line class-methods-use-this
public getMessage({ key, correctOrder }: { key: string; correctOrder: string[] }): string {
return `Property "${key}" is out of order. Expected order is: ${correctOrder.join(', ')}.`;
}

private readonly expectedOrder: TopLevelKeys[];

constructor(options?: TopLevelPropertiesOrderRuleOptions) {
this.expectedOrder = options?.customOrder ?? DEFAULT_ORDER;
}

public check(context: LintContext): LintMessage[] {
const errors: LintMessage[] = [];
const topLevelKeys = Object.keys(context.content);
Expand All @@ -75,7 +80,7 @@ export default class TopLevelPropertiesOrderRule implements LintRule {
const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort();

// Replace 'TopLevelKeys.XProperties' in the order with the actual sorted x-prefixed properties
const correctOrder = this.expectedOrder.flatMap((key) =>
const correctOrder = this.options.customOrder.flatMap((key) =>
key === TopLevelKeys.XProperties ? sortedXProperties : [key],
);

Expand Down Expand Up @@ -117,7 +122,7 @@ export default class TopLevelPropertiesOrderRule implements LintRule {

const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort();

const correctOrder = this.expectedOrder.flatMap((key) =>
const correctOrder = this.options.customOrder.flatMap((key) =>
key === TopLevelKeys.XProperties ? sortedXProperties : [key],
);

Expand Down
Loading

0 comments on commit 929123a

Please sign in to comment.