Skip to content

Commit 395de17

Browse files
authored
Fix: Make all method overloads get properly inherited into child classes (#150)
1 parent c4a295b commit 395de17

File tree

12 files changed

+284
-40
lines changed

12 files changed

+284
-40
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@webdoc/model",
5+
"comment": "Support for advanced variants in query API. For example, \"ns.className#method[params[0][dataType] = string]\" to get a specific method overload!",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@webdoc/model",
10+
"email": "[email protected]"
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@webdoc/parser",
5+
"comment": "Fix inheritance of overloaded methods. Prior to this fix, only the first overload was automatically inherited.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@webdoc/parser",
10+
"email": "[email protected]"
11+
}
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
declare module "@webdoc/model" {
22
import type {Doc, BaseDoc} from "@webdoc/types";
33

4-
declare function createDoc(name?: string, type?: string, options?: any): BaseDoc;
5-
declare function childDoc(lname: string, scope: BaseDoc): ?BaseDoc;
6-
declare function addChildDoc<T: BaseDoc>(doc: T, scope: BaseDoc): T;
7-
declare function doc(path: string | string[], root: BaseDoc): ?BaseDoc;
8-
declare function addDoc<T: BaseDoc>(doc: BaseDoc, root: BaseDoc): ?T;
4+
declare export function createDoc(name?: string, type?: string, options?: any): BaseDoc;
5+
declare export function childDoc(lname: string, scope: BaseDoc): ?BaseDoc;
6+
declare export function addChildDoc<T: BaseDoc>(doc: T, scope: BaseDoc): T;
7+
declare export function doc(path: string | string[], root: BaseDoc): ?BaseDoc;
8+
declare export function addDoc<T: BaseDoc>(doc: BaseDoc, root: BaseDoc): ?T;
9+
declare export function mangled(doc: Doc): string;
910
}

packages/webdoc-model/src/doc.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
// @flow
22

3-
import type {
4-
BaseDoc,
5-
Doc,
6-
DocLink,
7-
DocType,
8-
PackageDoc,
9-
RootDoc,
10-
TutorialDoc,
11-
} from "@webdoc/types";
3+
import type {BaseDoc, Doc, DocLink, DocType, PackageDoc, RootDoc, TutorialDoc} from "@webdoc/types";
124
import {nanoid} from "nanoid";
135

146
export const CANONICAL_SEPARATOR = /([.#~$])/g;
@@ -345,6 +337,22 @@ export function cloneDoc<T: Doc>(doc: T): T {
345337
});
346338
}
347339

340+
/**
341+
* Mangle the document name. If two documents have the same mangled name, then they shouldn't be
342+
* siblings (indicating a duplication).
343+
*
344+
* @param {Doc} doc
345+
* @return {string} - The mangled string name.
346+
*/
347+
export function mangled(doc: Doc): string {
348+
if (doc.type === "MethodDoc") {
349+
return `${doc.type}:${doc.name}:${
350+
doc.params.map((p) => p.dataType ? p.dataType[0] : "unknown").join("-")}`;
351+
}
352+
353+
return `${doc.type}:${doc.name}`;
354+
}
355+
348356
/**
349357
* You can pass this to traverse too.
350358
*/

packages/webdoc-model/src/query/parse.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
// @flow
22
// This file parses a raw query into a query model, as typed in types.js
33

4-
import type {
5-
QueryExpr,
6-
StepExpr,
7-
StepType,
8-
VariantCondition,
9-
VariantExpr,
10-
} from "./types";
4+
import type {Op, QueryExpr, StepExpr, StepType, VariantCondition, VariantExpr} from "./types";
115

126
// Regex to match step expression delimiters.
137
const STEP_EXPR_DELIMITER = /(?:[.][.][.])|(?:[.#~])/g;
148

159
// Regex to match variant expressions: [contents]
16-
const VARIANT_EXPR = /\[([^\]]+)\]/g;
10+
const VARIANT_EXPR = /\[([^\n]+)\]$/g;
1711

1812
// Condition operators
1913
const OP = /(?:=)|(?:>=)|(?:<=)|(?:>)|(?:<)/g;
@@ -23,10 +17,10 @@ function parseCondition(condition: string): VariantCondition {
2317
const opPattern = new RegExp(OP).exec(condition);
2418

2519
const op = opPattern ? opPattern[0] : "";
26-
const [attribute, value] = condition.split(OP);
20+
const [attribute, value] = condition.split(OP).map((expr) => expr.trim());
2721

2822
return {
29-
op,
23+
op: ((op: any): Op),
3024
attribute,
3125
value,
3226
};

packages/webdoc-model/src/query/query.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22

33
import type {Doc} from "@webdoc/types";
4-
import type {Query} from "./types";
4+
import type {QueryExpr} from "./types";
55
import {parse} from "./parse";
66
import {step} from "./step";
77
import {variant} from "./variant";
@@ -14,14 +14,14 @@ import {variant} from "./variant";
1414
* @param {Doc} docTree - The root of the document tree to be queried.
1515
* @return {Doc[]} The list of documents matching the query.
1616
*/
17-
export function query(queryExpr: string | Query, docTree: Doc): Doc[] {
17+
export function query(queryExpr: string | QueryExpr, docTree: Doc): Doc[] {
1818
queryExpr = typeof queryExpr === "string" ? parse(queryExpr) : queryExpr;
1919
let results: Doc[] = [docTree];
2020

2121
queryExpr.steps.forEach((stepExpr) => {
2222
results = results
2323
.flatMap((doc) => step(stepExpr, doc))
24-
.filter((doc) => variant(stepExpr.variant, doc));
24+
.filter((doc) => stepExpr.variant ? variant(stepExpr.variant, doc) : true);
2525
});
2626

2727
return results;

packages/webdoc-model/src/query/types.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
// @flow
2+
13
export type Op =
24
"=" |
35
">=" |
46
"<=" |
57
">" |
6-
">" |
8+
"<" |
79
"";
810

911
export type AttributeName = string;

packages/webdoc-model/src/query/variant.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
11
// @flow
22

3-
import {VariantCondition, VariantExpr} from "./types";
3+
import type {VariantCondition, VariantExpr} from "./types";
44
import type {Doc} from "@webdoc/types";
55

66
const operatorEngines = {
7-
"=": (a, b) => (a === b),
7+
"=": (a, b) => {
8+
// DataType comparison
9+
if (Array.isArray(a) && a.template && typeof b === "string") {
10+
return a[0] === b;
11+
}
12+
13+
return a === b;
14+
},
815
">=": (a, b) => (a >= b),
916
"<=": (a, b) => (a <= b),
1017
">": (a, b) => (a > b),
1118
"<": (a, b) => (a < b),
19+
"": (a, b) => true,
1220
};
1321

22+
const SUBINDEX = /([^\n]+)\[([^\n]+)\]$/;
23+
24+
function attribute(doc: any, attrib: string): any {
25+
const result = attrib.match(SUBINDEX);
26+
27+
if (result !== null) {
28+
const [, parentAttribute, index] = result;
29+
const parentValue = attribute(doc, parentAttribute);
30+
31+
if (typeof parentValue === "object" && typeof index === "string") {
32+
return parentValue[index];
33+
}
34+
}
35+
36+
return doc[attrib];
37+
}
38+
1439
function condition(conditionClause: VariantCondition, doc: Doc): boolean {
15-
const attributeValue = doc[conditionClause.attribute];
16-
const conditionValue = conditionClause.value;
40+
const attributeValue: any = attribute(doc, conditionClause.attribute);
41+
const conditionValue: any = conditionClause.value;
1742

1843
return operatorEngines[conditionClause.op](attributeValue, conditionValue);
1944
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const {
2+
addChildDoc,
3+
createDoc,
4+
createRootDoc,
5+
createSimpleKeywordType,
6+
query,
7+
} = require("../../lib");
8+
9+
const expect = require("chai").expect;
10+
11+
describe("@webdoc/model:query", function() {
12+
const documentTree = createRootDoc();
13+
const nsDoc = addChildDoc(createDoc("ns", "NSDoc"), documentTree);
14+
const parentClassDoc = addChildDoc(createDoc("ParentClass", "ClassDoc"), nsDoc);
15+
16+
addChildDoc(createDoc("invoke", "MethodDoc", {
17+
params: [{
18+
identifier: "arg0",
19+
dataType: createSimpleKeywordType("number"),
20+
description: "Arg 0",
21+
}],
22+
}), parentClassDoc);
23+
addChildDoc(createDoc("invoke", "MethodDoc", {
24+
params: [{
25+
identifier: "arg0",
26+
dataType: createSimpleKeywordType("string"),
27+
description: "Arg 0",
28+
}],
29+
}), parentClassDoc);
30+
31+
it("should discriminate based on parameter data type variant", function() {
32+
const m1Result = query("ns.ParentClass#invoke[params[0][dataType] = number]", documentTree);
33+
const m2Result = query("ns.ParentClass#invoke[params[0][dataType] = string]", documentTree);
34+
35+
expect(m1Result.length).to.be.greaterThan(0);
36+
expect(m2Result.length).to.be.greaterThan(0);
37+
expect(m1Result[0].params[0].dataType[0]).to.equal("number");
38+
expect(m2Result[0].params[0].dataType[0]).to.equal("string");
39+
});
40+
});

packages/webdoc-parser/src/transformer/mod-discover-members.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The discover-members mod imports member symbols from extended classes, implemented interfaces,
44
// and mixed mixins.
55

6-
import {addChildDoc, cloneDoc} from "@webdoc/model";
6+
import {addChildDoc, cloneDoc, mangled} from "@webdoc/model";
77
import type {Doc} from "@webdoc/types";
88

99
// The set of docs that discoverMembers has run on
@@ -48,13 +48,15 @@ function discoverMembers(doc: Doc, depsChain = new Set<Doc>()): void {
4848

4949
// This maps symbol names to the members of doc, so that the same symbol is not
5050
// inherited/overriden multiple times.
51-
const memberMap: { [id: string]: any } = {};
51+
const membersByMangledName: { [id: string]: any } = {};
52+
const membersByName: { [id: string]: any } = {};
5253

5354
// Prevent overridding symbols from being replaced by adding them beforehand.
5455
for (let i = 0; i < doc.members.length; i++) {
5556
const directMember = doc.members[i];
5657

57-
memberMap[directMember.name] = directMember;
58+
membersByMangledName[mangled(directMember)] = directMember;
59+
membersByName[directMember.name] = directMember;
5860
}
5961

6062
const parents = [];
@@ -83,6 +85,7 @@ function discoverMembers(doc: Doc, depsChain = new Set<Doc>()): void {
8385

8486
for (let i = 0; i < parent.members.length; i++) {
8587
const member = parent.members[i];
88+
const memberMangled = mangled(member);
8689

8790
// Only methods/properties/events are inheritable
8891
if (member.type !== "MethodDoc" &&
@@ -98,14 +101,23 @@ function discoverMembers(doc: Doc, depsChain = new Set<Doc>()): void {
98101
}
99102

100103
// Parent symbols are hidden by inherited/implemented symbols
101-
if (memberMap[member.name]) {
102-
memberMap[member.name].overrides = member;
104+
// Use mangled-map to try to find the most specific override
105+
if (membersByMangledName[memberMangled]) {
106+
membersByMangledName[memberMangled].overrides = member;
107+
continue;
108+
}
109+
// TODO: We might want to skip setting "overrides" in this case if it already overrides a
110+
// more important / primary signature.
111+
// Otherwise find by name
112+
// If the child class has another member with a different signature but same number
113+
if (membersByName[member.name]) {
114+
membersByName[member.name].overrides = member;
103115
continue;
104116
}
105117

106-
memberMap[member.name] = member;
107-
118+
// Only add an inherited member if the child has no member with the same name
108119
const temp = cloneDoc(member);
120+
membersByMangledName[memberMangled] = temp;
109121

110122
temp.overrides = false;
111123
temp.inherited = true;

0 commit comments

Comments
 (0)