Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ A = {
# introducesDeclarationLinkHash is the method — two hops, where TypeScript has boundFunctionLinkHash in one
boundNames=dict(file='all-javascript-variables.csv', name='name', only=lambda r: r.get('initializerKind') == 'FUNCTION',
via=dict(key='initializerExpressionLinkHash', file='all-javascript-expressions.csv', id='jsExpressionUniqueHash', method='introducesDeclarationLinkHash')),
# `exports.getUser = function …`, `obj.x = () => …`, `{ all: page => … }`: the function is named for the property it
# is the value of, the way the language's own name inference does it, and is owned by the object's variable
memberNames=dict(file='all-javascript-expressions.csv', id='jsExpressionUniqueHash', method='introducesDeclarationLinkHash',
vars='all-javascript-variables.csv'),
skipped='skipped-javascript-files.csv'),
'csharp': dict(
modules=dict(file='all-csharp-modules.csv', id='csModuleUniqueHash', filePath='filePath'),
Expand Down Expand Up @@ -254,6 +258,49 @@ if A['boundNames']:
else:
for r in rows(b['file']):
if r.get(b['method']): bound.setdefault(r[b['method']], r[b['name']])
# a function that is the value of a property takes the property's name: `recv.key = function …` (an ASSIGNMENT whose
# target is a non-computed property access) and `{ key: () => … }` (an OBJECT_LITERAL's PROPERTY_VALUE, keyed by the
# PROPERTY_KEY sibling at the same childIndex). The owner is the receiver as written (`helpers`, `Foo` for
# `Foo.prototype`), none for `exports` / `module.exports` / `this`; for a literal, the variable it initialises or the
# key chain of the literal it is nested in. A named function expression keeps its own name: only placeholders move.
bound_owner = {}
if A.get('memberNames'):
mn = A['memberNames']; ex = {}; kids = collections.defaultdict(list)
for r in rows(mn['file']):
ex[r[mn['id']]] = r; kids[r.get('parentExpressionLinkHash', '')].append(r)
lit_var = {r['initializerExpressionLinkHash']: r['name'] for r in rows(mn['vars']) if r.get('initializerExpressionLinkHash') and r.get('name')}
DOTTED = re.compile(r'^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$')
def key_of(parent, role, idx):
for k in kids.get(parent, ()):
if k.get('edgeRole') == role and (idx is None or k.get('childIndex') == idx):
return k
def literal_owner(lit, depth=0):
if lit.get(mn['id']) in lit_var: return lit_var[lit[mn['id']]]
p = ex.get(lit.get('parentExpressionLinkHash', ''))
if depth > 8 or not p or lit.get('edgeRole') != 'PROPERTY_VALUE' or p.get('expressionKind') != 'OBJECT_LITERAL': return None
k = key_of(p[mn['id']], 'PROPERTY_KEY', lit.get('childIndex'))
if not k or k.get('isComputedName') == 'true' or not k.get('name'): return None
o = literal_owner(p, depth + 1)
return f"{o}.{k['name']}" if o else k['name']
for r in ex.values():
m_ = r.get(mn['method'])
if not m_ or m_ in bound or r.get('expressionKind') != 'FUNCTION_EXPRESSION': continue
p = ex.get(r.get('parentExpressionLinkHash', ''))
if not p: continue
role = r.get('edgeRole'); name = owner = None
if role == 'ASSIGNMENT_VALUE' and p.get('expressionKind') == 'ASSIGNMENT' and p.get('operatorString') == '=':
t = key_of(p[mn['id']], 'ASSIGNMENT_TARGET', None)
if t and t.get('expressionKind') == 'PROPERTY_ACCESS' and t.get('isComputedName') != 'true' and t.get('name'):
name = t['name']; recv = (t.get('text') or '')[:-(len(name) + 1)]
if recv.endswith('.prototype'): recv = recv[:-len('.prototype')]
if DOTTED.match(recv) and recv not in ('exports', 'module.exports', 'this', 'module'): owner = recv
elif role == 'PROPERTY_VALUE' and p.get('expressionKind') == 'OBJECT_LITERAL':
k = key_of(p[mn['id']], 'PROPERTY_KEY', r.get('childIndex'))
if k and k.get('isComputedName') != 'true' and k.get('name'):
name = k['name']; owner = literal_owner(p)
if name:
bound[m_] = name
if owner: bound_owner[m_] = owner
# Java: a method declared in an enum CONSTANT's body (`Initial { boolean process(...) {…} }`) is registered per constant with the
# same qualified_name and the enum as owner — 23 `HtmlTreeBuilderState.process` rows in the JVM parser. The constant's span names it.
econst = collections.defaultdict(list)
Expand All @@ -270,7 +317,7 @@ bound_at = set() # (file, line, name) of a v
for m in c.execute("SELECT id, name, qualified_name, signature, kind, owner_type_id, owner_qualified_name, file_path, start_line, end_line FROM methods WHERE provenance='client'"):
name = m['name'] or ''
if name.startswith('<') and m['id'] in bound: name = bound[m['id']]; bound_at.add((rel(m['file_path']), m['start_line'], name))
od = tdisplay(m['owner_type_id']) if m['owner_type_id'] else None
od = tdisplay(m['owner_type_id']) if m['owner_type_id'] else bound_owner.get(m['id'])
if m['kind'] == 'ENUM_CONSTANT_METHOD' and od:
k_ = enum_constant_of(rel(m['file_path']), m['start_line'] or 0)
if k_: od = f"{od}.{k_}"
Expand Down
46 changes: 46 additions & 0 deletions tests/cases/javascript/member-assigned-function-names/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// exports.x = function / arrow: CommonJS controller shape
exports.getUser = function (req, res) {
return lookup(req.id);
};
exports.postUser = (req, res) => {
return lookup(req.body.id);
};
module.exports.slugify = function (s) {
return s.toLowerCase();
};

function lookup(id) {
return id;
}

// an object literal key bound to an arrow or a function expression
const Articles = {
all: page => request('/articles', page),
get: function (slug) {
return request('/articles/' + slug);
},
// CONTROL: method shorthand already keeps its name
del(slug) {
return request('/articles/' + slug, 'DELETE');
},
};

// obj.x = function
const helpers = {};
helpers.titleCase = function (s) {
return s.toUpperCase();
};

// CONTROL: a named function expression keeps its OWN name, not the key's
exports.alias = function realName(s) {
return s;
};

// CONTROL: an arrow passed as an argument has no name to take
[1, 2].map(n => lookup(n));

function request(url, arg) {
return url + arg;
}

module.exports.Articles = Articles;
48 changes: 48 additions & 0 deletions tests/cases/javascript/member-assigned-function-names/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"lang": "javascript",
"src": ".",
"checks": [
{
"why": "a function assigned to exports.x is named x: `impact getUser` answered `type getUser (not declared here)` because the function was `<function-expression>`",
"run": ["impact", "getUser"],
"want": ["change: getUser [method]", "[resolved] run use.js:4"],
"avoid": ["not declared here", "<function-expression>"]
},
{
"why": "an arrow assigned to exports.x, and a function to module.exports.x, are named for the property too",
"run": ["impact", "postUser", "slugify"],
"want": ["change: postUser [method]", "change: slugify [method]", "calls it (2 sites) [for postUser, slugify]"],
"avoid": ["not declared here", "<arrow>"]
},
{
"why": "an arrow that is the value of an object-literal key is named for the key and owned by the variable the literal initialises",
"run": ["impact", "Articles.all"],
"want": ["change: Articles.all [method]", "[resolved] run use.js:7"],
"avoid": ["nothing named"]
},
{
"why": "the bare key finds it as well, and a function expression under a key is named the same way",
"run": ["impact", "Articles.get"],
"want": ["change: Articles.get [method]", "[resolved] run use.js:8"],
"avoid": ["nothing named"]
},
{
"why": "obj.x = function is named x and owned by obj",
"run": ["impact", "titleCase"],
"want": ["change: helpers.titleCase [method]"],
"avoid": ["not declared here"]
},
{
"why": "CONTROL: a named function expression keeps its own name, not the property's",
"run": ["impact", "realName"],
"want": ["change: realName [method]"],
"avoid": ["alias"]
},
{
"why": "CONTROL: an arrow passed as an argument has no property to take a name from and stays a placeholder; method shorthand keeps its name",
"run": ["impact", "api.js:40", "del"],
"want": ["<arrow>", "del"],
"avoid": ["Articles.del", "map"]
}
]
}
12 changes: 12 additions & 0 deletions tests/cases/javascript/member-assigned-function-names/use.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const api = require('./api');

function run(req, res) {
api.getUser(req, res);
api.postUser(req, res);
api.slugify('A B');
api.Articles.all(1);
api.Articles.get('x');
api.Articles.del('x');
}

module.exports = { run };
Loading