Skip to content

Commit

Permalink
stash
Browse files Browse the repository at this point in the history
  • Loading branch information
runspired committed Oct 9, 2024
1 parent 25eabbf commit f371cde
Show file tree
Hide file tree
Showing 6 changed files with 662 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict';

// eslint-disable-next-line n/no-unpublished-require
const MAPPINGS = require('../../../libraries/warp-drive/mappings.json');
const WarpDrivePackages = ['ember-data', 'warp-drive'];

const WarpDriveOrgs = [
'@ember-data',
'@ember-data-types',
'@ember-data-mirror',
'@warp-drive',
'@warp-drive-types',
'@warp-drive-mirror',
];

function isBannedWarpDriveImport(imported) {
for (const pkg of WarpDrivePackages) {
if (imported === pkg || imported.startsWith(pkg + '/')) {
return true;
}
}
return false;
}

function isWarpDriveImport(imported) {
for (const org of WarpDriveOrgs) {
if (imported.startsWith(org + '/')) {
return true;
}
}
return false;
}

function convertToAuditBoardImport(imported) {
const parts = imported.split('/');
const [org, name] = parts;
const packageName = `${org}/${name}`;
const version = MAPPINGS.v0.includes(packageName)
? 'v0'
: MAPPINGS.v1.includes(packageName)
? 'v1'
: MAPPINGS.v2.includes(packageName)
? 'v2'
: 'none';

if (version === 'none') {
throw new Error(`Could not find an '@auditboard/warp-drive' mapping for ${packageName}}`);
}

const remainder = parts.slice(2).join('/');

// e.g. @warp-drive/foo => @auditboard/warp-drive/v2/foo
return remainder
? `'@auditboard/warp-drive/${version}/${name}/${remainder}'`
: `'@auditboard/warp-drive/${version}/${name}'`;
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description:
'disallow `import` / re-export directly from EmberData/WarpDrive projects in favor of @auditboard/warp-drive',
recommended: true,
},
type: 'problem',
fixable: 'code',
schema: [],
messages: {
bannedImport: `Importing from '{{ warpDriveName }}' is not allowed. Use a more specific import from '@auditboard/warp-drive' instead.`,
invalidImport: `Import from '{{ warpDriveName }}' should be changed to import from '{{ auditboardName }}'`,
},
},
create(context) {
function handleSpecifier(node) {
const imported = node.source?.value;

// exports can occur with values, not just from another module
if (!imported) {
return;
}

if (!isWarpDriveImport(imported)) {
return;
}

if (isBannedWarpDriveImport(imported)) {
context.report({
node: node.source,
messageId: 'bannedImport',
data: {
warpDriveName: imported,
},
});
}

const replaceWith = convertToAuditBoardImport(imported);
context.report({
node: node.source,
messageId: 'invalidImport',
data: { warpDriveName: imported, auditboardName: replaceWith },
*fix(fixer) {
yield fixer.replaceText(node.source, replaceWith);
},
});
}

return {
ImportDeclaration: handleSpecifier,
ExportAllDeclaration: handleSpecifier,
ExportNamedDeclaration: handleSpecifier,
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require inverse to be specified in @belongsTo and @hasMany decorators',
category: 'Ember Data',
recommended: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-async-inverse-relationship.md',
},
schema: [],
},

create(context) {
return {
CallExpression(node) {
const decorator =
node.parent.type === 'Decorator' &&
['belongsTo', 'hasMany'].includes(node.callee.name) &&
node;

if (decorator) {
const args = decorator.arguments;
const hasAsync = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some((prop) => prop.key.name === 'async')
);
const hasBooleanAsync = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some(
(prop) => prop.key.name === 'async' && typeof prop.value.value === 'boolean'
)
);
const hasInverse = args.some(
(arg) =>
arg.type === 'ObjectExpression' &&
arg.properties.some((prop) => prop.key.name === 'inverse')
);

if (!hasAsync) {
context.report({
node,
message: 'The @{{decorator}} decorator requires an `async` property to be specified.',
data: {
decorator: decorator.callee.name,
},
});
} else if (!hasBooleanAsync) {
context.report({
node,
message:
'The @{{decorator}} decorator requires an `async` property to be specified as a boolean.',
data: {
decorator: decorator.callee.name,
},
});
}

if (!hasInverse) {
context.report({
node,
message:
'The @{{decorator}} decorator requires an `inverse` property to be specified.',
data: {
decorator: decorator.callee.name,
},
});
}
}
},
};
},
};
109 changes: 109 additions & 0 deletions packages/eslint-plugin-ember-data/src/rules/no-legacy-data-patterns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

const STORE_METHOD_NAMES = new Set([
'findRecord',
'findAll',
'query',
'queryRecord',
'adapterFor',
'serializerFor',
'saveRecord',
'peekRecord',
'peekAll',
]);
const STORE_SERVICE_NAMES = new Set(['store', 'db', 'v2Store']);
const AJAX_SERVICE_NAMES = new Set(['apiAjax']);
const MODEL_METHOD_NAMES = new Set(['save', 'destroyRecord', 'reload']);
const RULE_ID = 'auditboard.warp-drive.no-legacy-data-patterns';

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
messages: {
[RULE_ID]: `Use \`store.request()\` instead of \`{{ objectName }}.{{propertyName}}()\``,
},
docs: {
description: 'Disallow use of legacy data patterns',
category: 'deprecation',
},
},

create(context) {
return {
CallExpression(node) {
// only match call expressions that are member expressions
// e.g. ignore `foo()`
if (node.callee.type !== 'MemberExpression') {
return;
}

// ignore computed expressions
// e.g. ignore `foo[bar]()`
if (node.callee.computed) {
return;
}

const propertyName = node.callee.property.name;

// ignore computed member expressions
// e.g. ignore `foo[bar].baz()`
if (node.callee.object.type === 'MemberExpression' && node.callee.object.computed) {
// unless we match one of MODEL_METHOD_NAMES
if (MODEL_METHOD_NAMES.has(propertyName)) {
context.report({
node,
messageId: RULE_ID,
data: { objectName: 'record', propertyName },
});
}
return;
}

const type = node.callee.object.type;

if (type !== 'ThisExpression' && type !== 'Identifier' && type !== 'MemberExpression') {
// anything else we just don't even wanna try
// for instance `/expr/.test(val)` is a valid call expression
return;
}

const objectName =
// store.findRecord()
node.callee.object.type === 'Identifier'
? node.callee.object.name
: // this.findRecord()
node.callee.object.type === 'ThisExpression'
? 'this'
: // this.store.findRecord()
node.callee.object.property.name;

if (AJAX_SERVICE_NAMES.has(objectName)) {
// all use of apiAjax is discouraged so we print this regardless of what the method is.
context.report({
node,
messageId: RULE_ID,
data: { objectName, propertyName },
});
return;
} else if (STORE_SERVICE_NAMES.has(objectName)) {
if (STORE_METHOD_NAMES.has(propertyName)) {
context.report({
node,
messageId: RULE_ID,
data: { objectName, propertyName },
});
}
return;
} else if (MODEL_METHOD_NAMES.has(propertyName)) {
context.report({
node,
messageId: RULE_ID,
data: { objectName, propertyName },
});
return;
}
},
};
},
};
Empty file.
Loading

0 comments on commit f371cde

Please sign in to comment.