Skip to content

Commit

Permalink
fix(resolve): split up the resolve labels
Browse files Browse the repository at this point in the history
  • Loading branch information
bzlibby authored and ssube committed Aug 30, 2020
1 parent c1b611c commit 804c3bb
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 123 deletions.
2 changes: 1 addition & 1 deletion docs/api/cautious-journey.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export { ChangeSet, FlagLabel, StateLabel, StateValue } from './labels';
export { Remote, RemoteOptions } from './remote';
export { GithubRemote } from './remote/github';
export { GitlabRemote } from './remote/gitlab';
export { ResolveInput, ResolveResult, resolveLabels } from './resolve';
export { ResolveInput, ResolveResult, resolveProject } from './resolve';
export { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';

const STATUS_ERROR = 1;
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export { FlagLabel, StateLabel } from './labels';
export { Remote, RemoteOptions } from './remote';
export { GithubRemote } from './remote/github';
export { GitlabRemote } from './remote/gitlab';
export { resolveLabels } from './resolve';
export { resolveProject } from './resolve';
export { syncIssueLabels, syncProjectLabels } from './sync';

const ARGS_START = 2;
Expand Down
219 changes: 115 additions & 104 deletions src/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { doesExist } from '@apextoaster/js-utils';

import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel } from './labels';
import { BaseLabel, FlagLabel, getValueName, prioritySort, StateLabel, StateValue } from './labels';
import { defaultUntil } from './utils';

/**
Expand Down Expand Up @@ -61,131 +61,142 @@ export interface ResolveResult {
* Resolve the desired set of labels, given a starting set and the flags/states to be
* applied.
*/
/* eslint-disable-next-line sonarjs/cognitive-complexity */
export function resolveLabels(options: ResolveInput): ResolveResult {
const activeLabels = new Set(options.labels);
const changes: Array<ChangeRecord> = [];
const errors: Array<ErrorRecord> = [];
function resolveBaseLabel(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>) {
if (activeLabels.has(label.name) === false) {
return true;
}

for (const requiredLabel of label.requires) {
if (!activeLabels.has(requiredLabel.name)) {
if (activeLabels.delete(label.name)) {
anticipatedResult.changes.push({
cause: requiredLabel.name,
effect: ChangeVerb.REQUIRED,
label: label.name,
});
}

function checkLabelRules(label: BaseLabel) {
if (activeLabels.has(label.name) === false) {
return true;
}
}

for (const requiredLabel of label.requires) {
if (!activeLabels.has(requiredLabel.name)) {
if (activeLabels.delete(label.name)) {
changes.push({
cause: requiredLabel.name,
effect: ChangeVerb.REQUIRED,
label: label.name,
});
}

return true;
}
for (const addedLabel of label.adds) {
// Set.add does not return a boolean, unlike the other methods
if (!activeLabels.has(addedLabel.name)) {
activeLabels.add(addedLabel.name);
anticipatedResult.changes.push({
cause: label.name,
effect: ChangeVerb.CREATED,
label: addedLabel.name,
});
}
}

for (const removedLabel of label.removes) {
if (activeLabels.delete(removedLabel.name)) {
anticipatedResult.changes.push({
cause: label.name,
effect: ChangeVerb.REMOVED,
label: removedLabel.name,
});
}
}

for (const addedLabel of label.adds) {
// Set.add does not return a boolean, unlike the other methods
if (!activeLabels.has(addedLabel.name)) {
activeLabels.add(addedLabel.name);
changes.push({
cause: label.name,
effect: ChangeVerb.CREATED,
label: addedLabel.name,
return false;
}

function resolveBecomes(label: BaseLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>, value: StateValue): boolean {
for (const become of value.becomes) {
const matches = become.matches.every((l) => activeLabels.has(l.name));

if (matches) {
resolveBaseLabel({
...label,
adds: become.adds,
removes: [...become.matches, ...become.removes],
requires: [],
}, anticipatedResult, activeLabels);

if (activeLabels.delete(name)) {
anticipatedResult.changes.push({
cause: name,
effect: ChangeVerb.REMOVED,
label: name,
});
}
return true;
}
}
return false;
}

for (const removedLabel of label.removes) {
if (activeLabels.delete(removedLabel.name)) {
changes.push({
cause: label.name,
effect: ChangeVerb.REMOVED,
label: removedLabel.name,
/**
* Need to ensure that there is only 1 active value for the state
* If no, remove any lower priority active values for the state
* Need to run the normal (add, remove) rules
* Need to run the becomes rules
*/
function resolveState(state: StateLabel, anticipatedResult: ResolveResult, activeLabels: Set<string>) {
let activeValue;

const sortedValues = prioritySort(state.values);
for (const value of sortedValues) {
const name = getValueName(state, value);
if (!activeLabels.has(name)) {
continue;
}

if (doesExist(activeValue)) { // there is already an active value
if (activeLabels.delete(name)) {
anticipatedResult.changes.push({
cause: name,
effect: ChangeVerb.CONFLICTED,
label: name,
});
}

continue;
}

const combinedValue: BaseLabel = {
adds: [...state.adds, ...value.adds],
name,
priority: defaultUntil(value.priority, state.priority, 0),
removes: [...state.removes, ...value.removes],
requires: [...state.requires, ...value.requires],
};

if (resolveBaseLabel(combinedValue, anticipatedResult, activeLabels)) {
continue;
}

if (resolveBecomes(combinedValue, anticipatedResult, activeLabels, value)) {
continue;
}

return false;
activeValue = name;
}
}

export function resolveProject(options: ResolveInput): ResolveResult {
const result: ResolveResult = {
changes: [],
errors: [],
labels: [],
};
const activeLabels = new Set(options.labels);

const sortedFlags = prioritySort(options.flags);
for (const flag of sortedFlags) {
checkLabelRules(flag);
resolveBaseLabel(flag, result, activeLabels);
}

const sortedStates = prioritySort(options.states);
for (const state of sortedStates) {
let activeValue;

const sortedValues = prioritySort(state.values);
for (const value of sortedValues) {
const name = getValueName(state, value);
if (activeLabels.has(name)) {
if (doesExist(activeValue)) {
if (activeLabels.delete(name)) {
changes.push({
cause: name,
effect: ChangeVerb.CONFLICTED,
label: name,
});
}

continue;
}

const combinedValue: BaseLabel = {
adds: [...state.adds, ...value.adds],
name,
priority: defaultUntil(value.priority, state.priority, 0),
removes: [...state.removes, ...value.removes],
requires: [...state.requires, ...value.requires],
};

if (checkLabelRules(combinedValue)) {
continue;
}

// TODO: flatten this bit and remove the mutable boolean
let removed = false;
for (const become of value.becomes) {
const matches = become.matches.every((l) => activeLabels.has(l.name));

if (matches) {
checkLabelRules({
...combinedValue,
adds: become.adds,
removes: [...become.matches, ...become.removes],
requires: [],
});

if (activeLabels.delete(name)) {
changes.push({
cause: name,
effect: ChangeVerb.REMOVED,
label: name,
});
removed = true;
}

break;
}

if (removed) {
continue;
}

activeValue = name;
}
}
}
resolveState(state, result, activeLabels);
}

return {
changes,
errors,
labels: Array.from(activeLabels),
};
result.labels = Array.from(activeLabels);

return result;
}
4 changes: 2 additions & 2 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { prng } from 'seedrandom';
import { ProjectConfig } from './config';
import { getLabelColor, getLabelNames, getValueName } from './labels';
import { LabelUpdate, Remote } from './remote';
import { resolveLabels } from './resolve';
import { resolveProject } from './resolve';
import { compareItems, defaultTo, defaultUntil } from './utils';

export interface SyncOptions {
Expand All @@ -28,7 +28,7 @@ export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
for (const issue of issues) {
logger.info({ issue }, 'project issue');

const { changes, errors, labels } = resolveLabels({
const { changes, errors, labels } = resolveProject({
flags: project.flags,
labels: issue.labels,
states: project.states,
Expand Down
8 changes: 4 additions & 4 deletions test/TestResolve.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { expect } from 'chai';

import { resolveLabels } from '../src/resolve';
import { resolveProject } from '../src/resolve';
import { TEST_CASES } from './resolve/cases';

const TEST_LABELS = ['foo', 'bar'];

describe('resolve labels', () => {
describe('with empty rule set', () => {
it('should return the existing labels', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [],
labels: TEST_LABELS,
states: [],
Expand All @@ -18,7 +18,7 @@ describe('resolve labels', () => {
});

it('should not make any changes', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [],
labels: TEST_LABELS,
states: [],
Expand All @@ -32,7 +32,7 @@ describe('resolve labels', () => {
describe('resolver test cases', () => {
for (const test of TEST_CASES) {
it(`should resolve ${test.name}`, () => {
const actualResult = resolveLabels(test.input);
const actualResult = resolveProject(test.input);
expect(actualResult).to.deep.equal(test.result);
});
}
Expand Down
10 changes: 5 additions & 5 deletions test/resolve/TestResolveIssueLabels.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expect } from 'chai';

import { resolveLabels } from '../../src/resolve';
import { resolveProject } from '../../src/resolve';

const TEST_LABELS = ['foo', 'bar'];

describe('resolve labels', () => {
describe('flags with unfulfilled requires rule', () => {
it('should be removed when required label is missing', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [{
adds: [],
name: 'gayle',
Expand All @@ -27,7 +27,7 @@ describe('resolve labels', () => {

describe('flags with fulfilled requires rule', () => {
it('should make no changes', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [{
adds: [],
name: 'gayle',
Expand All @@ -47,7 +47,7 @@ describe('resolve labels', () => {

describe('flags with add rules', () => {
it('should add the labels', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [{
adds: [{
name: 'linda',
Expand All @@ -67,7 +67,7 @@ describe('resolve labels', () => {

describe('flags with remove rules', () => {
it('should remove labels', () => {
const result = resolveLabels({
const result = resolveProject({
flags: [{
adds: [],
name: 'bob',
Expand Down

0 comments on commit 804c3bb

Please sign in to comment.