-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
662 additions
and
0 deletions.
There are no files selected for viewing
114 changes: 114 additions & 0 deletions
114
packages/eslint-plugin-ember-data/src/rules/no-direct-warp-drive-import.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}, | ||
}; |
75 changes: 75 additions & 0 deletions
75
packages/eslint-plugin-ember-data/src/rules/no-invalid-relationships.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
109
packages/eslint-plugin-ember-data/src/rules/no-legacy-data-patterns.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.